关于 Flutter 文档中文版

目录

Flutter 的魅力之一是开源开放,Flutter 由 Google 主导,与全球的开发者共同协作。中国的开发者数量占全球的 20% 以上,也有大量贡献者为 Flutter 的生态提供了很大的帮助和贡献。请查看这个视频,了解更多 Flutter 中国开发者的情况

关于 Flutter 文档中文版

这是一份由中国 Flutter 社区开发者们协作完成、与官方保持同步更新的中文文档,希望能够帮助国内、以及全球讲中文的开发者更好的了解 Flutter。

点击文档的中文会显示英文原文,这能帮助你更全面的理解文档、体会文档作者希望传达的内容,也同时方便大家为文档里晦涩难懂的中文翻译提供对比、纠错和完善。

感谢名单

Flutter 中国社区的活跃壮大离不开每一位为社区贡献的人,在这些对社区充满热情的人们共同努力下,中国的社区蓬勃发展,谢谢大家!

非常感谢下述提到的 Leadership 团队、公司 / 社区合作伙伴、中文文档译者团队、Google 团队等人:

中文文档译者团队

  1. 感谢 Xinlei Wang 对 Flutter 中文文档的翻译、维护和在社区中活跃解答大家的问题,感谢中文文档翻译项目的运营志愿者 @jin-zz 和 @hayley1643,感谢上海 GDG 的 Fei Feng 和 Ping Ma。
  2. 感谢掘金翻译计划协调资源帮助完成 Flutter 文档本地化项目的启动 (2019/4/16);
  3. 感谢阅文前端团队协调资源帮助完成 Flutter 文档本地化项目第一阶段的收尾 (2019/6/21);
  4. 感谢持续维护 Dart 中文文档的 Haijun Gu (@amisare),感谢他允许并帮忙将已经翻译的内容贡献到 dart.cn (2019);
  5. 感谢参与 Flutter / Dart 文档本地化项目的志愿者们:@5ibinbin, @AigeStudio, @amisare, @ASkyBig, @bestony, @changer0, @chenglu, @chunlea, @Dong09, @DongXYZ, @Dosimz, @duxinfeng, @ElderJames, @EvilLee6, @gejiaheng, @git-patrickliu, @guanxf, @zhang-honghao, @hijiangtao, @Iamnotromantic, @iCell, @inferjay, @JasonLinkinBright, @Jenniferyingni, @jin-zz, @krave1986, @lennonover, @linguowu, @linhou, @liuzhen0218, @LyndonChin, @MzoneCL, @nanjingboy, @nervouself, @nesger, @noahziheng, @panda8z, @phxnirvana, @qiuyuezhong, @reachzhai, @Realank, @realcarlos, @Robinhaizhou, @shengxinyuan, @songfei, @SouthernBox, @talisk, @tyisme614, @Vadaski, @vinciarts, @wonderful89, @wswdevil, @x531tanxl, @XatMassacrE, @xilihuasi, @xiongcai, @yantao13145210, @yanxin, @yanyixin, @yeqiling, @youngyou, @Yuan, @Zhangdroid, @zhangjiana, @Zhiw, @zhuangtao97, @ziank, @DaoXingHuang, @qingmei2, @CaiJingLong, @toly1994328, @yumi0629 等 (2018 - 2020)。

CFUG 团队

  1. 感谢 Adam Yi, Zhongdong Yang, Xiaoyu Li, Shena Bian, Bing Gao, Steven Tian 以及 Study Jams 学员和谷创字幕组译者在 18 年 5 月份加班加点完成初版 Flutter Codelabs 和 Flutter 中文社区资源网站的设计;
  2. Shena Bian, Yihui Miao 对 Flutter 中文社区资源网址的设计 (2018 - 2020);
  3. 感谢 Zhongdong Yang 对 Flutter 社区中文资源网站 flutter.cn, codelabs.flutter-io.cn 的前端技术实现 (2018 - 2019);
  4. 感谢雪狼帮助适配、调试和制作双语对照的翻译工具以及 debug server 的 CI 调试部署,感谢 @Zheaoli 帮忙搭建和调试 CI 以及文档部署方案 (2018 - 2019);
  5. 感谢 @eatmiya 维护 Flutter 社区微信公众号 (2019);
  6. 感谢 Alex Li (@AlexV525) 和加康 (@MeandNi) 加入贡献 CFUG 团队,大量参与社区的讨论并实质性推动了本地化项目进程,感谢 Alex 为 Flutter 项目提交 PR 并成功加入成为 Flutter members! (2020);
  7. 感谢加康 (@MeandNi) 出版 Flutter 书籍《Flutter 开发之旅从南到北》,并在公众号撰写文章积极投稿到谷歌开发者的 DTalk 项目 (2020);
  8. 感谢 Xinlei Wang 介绍 Alex 和加康加入 CFUG 团队 (2020)。

Google 公司和合作伙伴团队

  1. 特别感谢 Google Flutter 团队里一直对中国社区支持并作出卓越贡献的 Tao Dong, Xiao Yu 和 Yuqian Li,感谢前 DevRel 团队 Lynn Wang 在 Flutter 社区里的活跃和积极贡献,感谢谷歌开发者运营团队的 Anna (2019) 和 Smile (2020);
  2. 感谢 360 前端委员会的 Shuo He 和 360 大学的 Weiyang Zhang 举办的 360 互联网技术训练营 Flutter 专场,感谢声网的 Xingxing Qin 和 Olivia,感谢 RTC 开发者社区和 GDG 社区积极举办多次与 Flutter 相关的活动 (2019);
  3. 感谢各大公司 / 团队的社区联系人帮助在内部分享 Flutter 和推动 Flutter 的落地,及时有效的安排 Googler 的拜访,内部的技术问题的收集和优先级排序,以及在各种大会上出席做演讲认可和推广 Flutter (2019);
  4. 感谢阿里巴巴闲鱼的宗心、KyleWong 和树彬,感谢一直对 Flutter 的代码、文章等资源贡献,感谢开源 Fish Redux 和 FlutterBoost,感谢在阿里内部推动 Flutter (2019);
  5. 感谢阿里拍卖前端团队开源的 flutter-go (2019);
  6. 感谢快手的 Kai Sun 引荐公司团队与 Flutter 团队积极沟通,并多次参与社区演讲 (2019);

谷歌活动 / 社区活动志愿者

  1. 感谢 GMTC 的活动组织者和主办方给予 Flutter 如此大量的曝光和内容展示,以及在 InfoQ 旗下众多开发者公众号里大力宣传 Flutter (2018 - 2019);
  2. 感谢 Xinlei Wang 组织了 Flutter 成都高校系列活动(2018 年 12 月);
  3. 感谢 Xinlei Wang, Congli Ma, Yanbo Liu (Flutter GDE), Bill Fu (TikTok 团队) 在 GDD 大会现场的 Flutter 展位站台并回答大家的问题(2019/9/11)。
  4. 感谢 JetBrains 开发者关系团队的 Shengyou Fan 组织「Flutter x Ktor 打造跨平台全端应用」网络研讨会,CFUG 社区的 Xinlei Wang 受邀参与作为联合主讲人 (2020)。

社区贡献者(暂未分类)

  1. 感谢 Qinglian Zhang 和 Wen Du 在最初 Flutter 社区和资源匮乏的情况下,建立了 Flutter 交流论坛、翻译了 Flutter 中文文档(2018 年初);
  2. 感谢掘金社区的创始人 Glow Chiang 对 Flutter 社区的无限大力支持,包括 2018 年 8 月份的 Flutter 征文大赛,Flutter 相关的掘金小册。感谢掘金翻译计划负责人 Xuewen Ding 给予的帮助和支持;
  3. 感谢宁皓网创始人宁皓和技术胖在国内推出 Flutter 课程教学并参与 Flutter 1.0 谷歌北京发布活动(2018/12/4);
  4. 感谢 Sijie Cheng 引荐全国各大院校的开源协会 (2019 年 1 月),建立领导 Flutter x 高校团队,为 Flutter 在清华、北大、中科大、上海大学、重庆大学等建立镜像提供了可能,感谢清华大学 TUNA 协会的 Yuxiang Zhang, Yiqun Hui 帮助在清华大学开源镜像站加入 Flutter 镜像 & 实现 Flutter Pub site API 的同步策略;
  5. 感谢思否社区的创始人 Sunny Gao 帮助我们在思否建立 Flutter 标签 (2019 年 9 月);
  6. 感谢前 GDG 组织者,现郑州玩码科技负责人 @inferjay 帮助维护 flutter-io.cn / dartpad.cn 等域名和基建资源,感谢为谷歌活动构建 AI 体验馆报名系统的千跃优意 (Cheerue) 创始人大树,帮助维护 flutter.cn / dart.cn / material-io.cn 等域名和基建资源 (2019 至今);
  7. 感谢 OpenWrite 团队开发出多平台文章同步系统 (2019);
  8. 感谢上海交通大学 Linux 用户组的 Alex Chi (@skyzh) 组织并修复 SJTUG Flutter 镜像 (2020)。
  9. 感谢 七牛云 为中国镜像提供支持和赞助 (2021)。

特别感谢的社区名单

  1. 北京、上海、广州、深圳 GDG 社区;
  2. RTC 开发者社区;
  3. 360 大学;
  4. 掘金社区;
  5. 思否社区;
  6. 奇舞团;
  7. 上海交通大学 Linux 用户组;
  8. 清华大学 TUNA 协会;
  9. 上海大学 Linux 用户组;
  10. 重庆大学蓝盟团队。

感谢在 GitHub 上为 flutter 贡献、解答 issues,提交 PR 的开发者们,由于这部分贡献并非由我们主导,所以仅在此表示感谢,感谢你们为 Flutter 这样一个优秀产品所做出的努力和贡献!

以上内容多数以贡献开始时间排序,感谢名单、机构顺序不分前后。

安装和环境配置

你想把 Flutter 安装在哪个操作系统呢?

Select the operating system on which you are installing Flutter:

Windows
macOS
Linux
Chrome OS

编辑工具设定

你可以使用任意文本编辑器,结合我们的命令行工具来开发 Flutter 应用。然而,我们推荐使用我们的编辑器插件以获取更好的开发体验。这些插件提供了代码补全、代码高亮、widget 辅助编辑的功能,以及为项目的运行和调试提供支持等。

You can build apps with Flutter using any text editor combined with our command-line tools. However, we recommend using one of our editor plugins for an even better experience. These plugins provide you with code completion, syntax highlighting, widget editing assists, run & debug support, and more.

参考以下步骤为 Android Studio、IntelliJ 或者 VS Code 添加编辑器插件。如果你想使用其他的编辑器,请直接打开 下一节: 开发体验初探,来查看使用其他文本编辑器配合命令行工具来创建和运行 Flutter 应用。

Follow the steps below to add an editor plugin for Android Studio, IntelliJ, VS Code, or Emacs. If you want to use a different editor, that’s OK, skip ahead to the next step: Test drive.

安装 Android Studio

Install Android Studio

Android Studio 为 Flutter 提供了一个完整的集成开发环境。

Android Studio offers a complete, integrated IDE experience for Flutter.

同时, 你也可以使用 IntelliJ:

Alternatively, you can also use IntelliJ:

安装 Flutter 和 Dart 插件

Install the Flutter and Dart plugins

请参考下面不同平台的安装指南:

The installation instructions vary by platform.

Mac

安装过程如下:

Use the following instructions for macos:

  1. 打开 Android Studio。

    Start Android Studio.

  2. 打开插件设置(在 v3.6.3.0 以上的系统打开 Preferences > Plugins)。

    Open plugin preferences (Preferences > Plugins as of v3.6.3.0 or later).

  3. 然后选择 Flutter 插件并点击 安装

    Select the Flutter plugin and click Install.

  4. 当弹出安装 Dart 插件提示时,点击 Yes

    Click Yes when prompted to install the Dart plugin.

  5. 当弹出重新启动提示时,点击 Restart

    Click Restart when prompted.

Linux 或者 Windows 平台

Linux or Windows

参考使用下面介绍的步骤:

Use the following instructions for Linux or Windows:

  1. 打开插件偏好设置 (位于 File > Settings > Plugins)

    Open plugin preferences (File > Settings > Plugins).

  2. 选择 Marketplace (扩展商店),选择 Flutter plugin 然后点击 Install (安装)

    Select Marketplace, select the Flutter plugin and click Install.

安装 VS Code

Install VS Code

VS Code 是一个可以运行和调试 Flutter 的轻量级编辑器。

VS Code is a light-weight editor with Flutter app execution and debug support.

安装 Flutter 和 Dart 插件

Install the Flutter and Dart plugins

  1. 打开 VS Code。

    Start VS Code.

  2. 打开 查看 > 命令面板…

    Invoke View > Command Palette….

  3. 输入 “install”,然后选择 扩展: 安装扩展

    Type “install”, and select Extensions: Install Extensions.

  4. 在扩展搜索输入框中输入 “flutter”,然后在列表中选择 Flutter 并单击 安装。此过程中会自动安装必需的 Dart 插件。

    Type “flutter” in the extensions search field, select Flutter in the list, and click Install. This also installs the required Dart plugin.

  5. 点击 重新加载 以重新启动 VS Code。

    Click Reload to Activate to reload VS Code.

通过 Flutter Doctor 命令验证是否安装成功

Validate your setup with the Flutter Doctor

  1. 打开 查看 > 命令面板…

    Invoke View > Command Palette….

  2. 输入 “doctor”,选择 Flutter: Run Flutter Doctor

    Type “doctor”, and select the Flutter: Run Flutter Doctor.

  3. 打开 输出 (OUTPUT) 面板查看是否有错误,确保在不同的输出选项 (Output Options) 的下拉列表中选择了 Flutter。

    Review the output in the OUTPUT pane for any issues. Make sure to select Flutter from the dropdown in the different Output Options.

安装 Emacs 编辑器

Install Emacs

Emacs 是一个轻量级的编辑器,支持 Flutter 和 Dart。

Emacs is a lightweight editor with support for Flutter and Dart.

  • 最新版本的 Emacs 编辑器。

    Emacs, latest stable version

安装 lsp-dart 包

Install the lsp-dart package

关于如何安装和使用 lsp-dart 包,可以查看 lsp-dart 文档

For information on how to install and use the package, see the lsp-dart documentation.

开发体验初探

本页面讲解如何通过模板实现一个 Flutter 应用,执行并且在修改程序之后触发“热重载 (hot reload)”功能。

This page describes how to create a new Flutter app from templates, run it, and experience “hot reload” after you make changes to the app.

选择你用于编写、编译、执行 Flutter 应用的开发环境吧。

Select your development tool of choice for writing, building, and running Flutter apps.

Create the app

  1. Open the IDE and select Create New Flutter Project.
  2. Select Flutter Application as the project type. Then click Next.
  3. Verify the Flutter SDK path specifies the SDK’s location (select Install SDK… if the text field is blank).
  4. Enter a project name (for example, myapp). Then click Next.
  5. Click Finish.
  6. Wait for Android Studio to install the SDK and create the project.

The above commands create a Flutter project directory called myapp that contains a simple demo app that uses Material Components.

Run the app

  1. Locate the main Android Studio toolbar:
    Main IntelliJ toolbar
  2. In the target selector, select an Android device for running the app. If none are listed as available, select Tools > AVD Manager and create one there. For details, see Managing AVDs.
  3. Click the run icon in the toolbar, or invoke the menu item Run > Run.

当应用编译完成后,就可以在设备上运行这个起步应用了。

After the app build completes, you’ll see the starter app on your device.

Starter app on iOS
Starter app

尝试热重载 (hot reload)

Try hot reload

Flutter 通过 热重载 提供快速开发周期,该功能支持应用程序在运行状态下重载代码而无需重新启动应用程序或者丢失程序运行状态。修改一下代码,然后告诉IDE或者命令行工具你需要热重载,然后看一下模拟器或者设备上应用的变化

Flutter offers a fast development cycle with Stateful Hot Reload, the ability to reload the code of a live running app without restarting or losing app state. Make a change to app source, tell your IDE or command-line tool that you want to hot reload, and see the change in your simulator, emulator, or device.

  1. 打开 lib/main.dart

    Open lib/main.dart.

  2. 修改字符串

    Change the string

    'You have pushed the button this many times'

    改为

    to

    'You have clicked the button this many times'
  3. 保存修改

    Save your changes : invoke Save All, or click Hot Reload lightning bolt .

你会发现修改后的字符串几乎马上出现在正在运行的应用程序上。

You’ll see the updated string in the running app almost immediately.

以 profile 模式运行

Profile or release runs

截止目前文档所示内容,你的应用应该运行在调试 (debug) 模式中,这个模式意味着在更大的性能开销下实现了更快速的开发效率,比如热重载功能的启用,因此你可能要面临较差质量的动画效果。当你准备分析应用性能或要打包发布的时候,你可能需要 Flutter 的 profile 或者 release 构建,相关文档,请查阅文档: Flutter 的构建模式选择

So far you’ve been running your app in debug mode. Debug mode trades performance for useful developer features such as hot reload and step debugging. It’s not unexpected to see slow performance and janky animations in debug mode. Once you are ready to analyze performance or release your app, you’ll want to use Flutter’s “profile” or “release” build modes. For more details, see Flutter’s build modes.

Create the app

  1. Invoke View > Command Palette.
  2. Type “flutter”, and select the Flutter: New Project.
  3. Select Application.
  4. Create or select the parent directory for the new project folder.
  5. Enter a project name, such as myapp, and press Enter.
  6. Wait for project creation to complete and the main.dart file to appear.

The above commands create a Flutter project directory called myapp that contains a simple demo app that uses Material Components.

Run the app

  1. Locate the VS Code status bar (the blue bar at the bottom of the window):
    status bar
  2. Select a device from the Device Selector area. For details, see Quickly switching between Flutter devices.
    • If no device is available and you want to use a device simulator, click No Devices and launch a simulator.

    • To setup a real device, follow the device-specific instructions on the Install page for your OS.

  3. Invoke Run > Start Debugging or press F5.
  4. Wait for the app to launch — progress is printed in the Debug Console view.

当应用编译完成后,就可以在设备上运行这个起步应用了。

After the app build completes, you’ll see the starter app on your device.

Starter app on iOS
Starter app

尝试热重载 (hot reload)

Try hot reload

Flutter 通过 热重载 提供快速开发周期,该功能支持应用程序在运行状态下重载代码而无需重新启动应用程序或者丢失程序运行状态。修改一下代码,然后告诉IDE或者命令行工具你需要热重载,然后看一下模拟器或者设备上应用的变化

Flutter offers a fast development cycle with Stateful Hot Reload, the ability to reload the code of a live running app without restarting or losing app state. Make a change to app source, tell your IDE or command-line tool that you want to hot reload, and see the change in your simulator, emulator, or device.

  1. 打开 lib/main.dart

    Open lib/main.dart.

  2. 修改字符串

    Change the string

    'You have pushed the button this many times'

    改为

    to

    'You have clicked the button this many times'
  3. 保存修改

    Save your changes : invoke Save All, or click Hot Reload lightning bolt .

你会发现修改后的字符串几乎马上出现在正在运行的应用程序上。

You’ll see the updated string in the running app almost immediately.

以 profile 模式运行

Profile or release runs

截止目前文档所示内容,你的应用应该运行在调试 (debug) 模式中,这个模式意味着在更大的性能开销下实现了更快速的开发效率,比如热重载功能的启用,因此你可能要面临较差质量的动画效果。当你准备分析应用性能或要打包发布的时候,你可能需要 Flutter 的 profile 或者 release 构建,相关文档,请查阅文档: Flutter 的构建模式选择

So far you’ve been running your app in debug mode. Debug mode trades performance for useful developer features such as hot reload and step debugging. It’s not unexpected to see slow performance and janky animations in debug mode. Once you are ready to analyze performance or release your app, you’ll want to use Flutter’s “profile” or “release” build modes. For more details, see Flutter’s build modes.

创建应用程序

Create the app

使用 flutter create 命令来创建新的工程:

Use the flutter create command to create a new project:

$ flutter create myapp
$ cd myapp

It is also possible to pass other arguments to flutter create, such as the project name (pubspec.yml), the organization name, or to specify the programming language used for the native platform:

$ flutter create --project-name myapp --org dev.flutter --android-language java --ios-language objc myapp
$ cd myapp

该命令会创建一个名为 myapp,里面包含一个简单的示例程序,里面用到了 Material 组件

The command creates a Flutter project directory called myapp that contains a simple demo app that uses Material Components.

运行程序

Run the app

  1. 检查一下 Android 设备是否已经正常运行。如果程序未显示,请在 安装 页面里,根据你的操作系统按照设备相关说明进行操作。

    Check that an Android device is running. If none are shown, follow the device-specific instructions on the Install page for your OS.

    $ flutter devices
    
  2. 使用下面指令运行应用程序:

    Run the app with the following command:

    $ flutter run
    

当应用编译完成后,就可以在设备上运行这个起步应用了。

After the app build completes, you’ll see the starter app on your device.

Starter app on iOS
Starter app

尝试热重载 (hot reload)

Try hot reload

Flutter 通过 热重载 提供快速开发周期,该功能支持应用程序在运行状态下重载代码而无需重新启动应用程序或者丢失程序运行状态。修改一下代码,然后告诉IDE或者命令行工具你需要热重载,然后看一下模拟器或者设备上应用的变化

Flutter offers a fast development cycle with Stateful Hot Reload, the ability to reload the code of a live running app without restarting or losing app state. Make a change to app source, tell your IDE or command-line tool that you want to hot reload, and see the change in your simulator, emulator, or device.

  1. 打开 lib/main.dart

    Open lib/main.dart.

  2. 修改字符串

    Change the string

    'You have pushed the button this many times'

    改为

    to

    'You have clicked the button this many times'
  3. Save your changes .

  4. 在命令行窗口输入 r

    保存修改

    Type r in the terminal window.

你会发现修改后的字符串几乎马上出现在正在运行的应用程序上。

You’ll see the updated string in the running app almost immediately.

以 profile 模式运行

Profile or release runs

截止目前文档所示内容,你的应用应该运行在调试 (debug) 模式中,这个模式意味着在更大的性能开销下实现了更快速的开发效率,比如热重载功能的启用,因此你可能要面临较差质量的动画效果。当你准备分析应用性能或要打包发布的时候,你可能需要 Flutter 的 profile 或者 release 构建,相关文档,请查阅文档: Flutter 的构建模式选择

So far you’ve been running your app in debug mode. Debug mode trades performance for useful developer features such as hot reload and step debugging. It’s not unexpected to see slow performance and janky animations in debug mode. Once you are ready to analyze performance or release your app, you’ll want to use Flutter’s “profile” or “release” build modes. For more details, see Flutter’s build modes.

编写第一个 Flutter 应用

目录

If you prefer an instructor-led version of this codelab, check out the following workshop:

The app that you'll be building

这是一个指引你完成第一个 Flutter 应用的手把手操作教程(我们也称之为是 codelab)。我们将会着手创建一个简单的 Flutter 应用,无需 Dart 语言、移动开发语言或 Web 开发经验,只需你具备面向对象语言开发基础即可(如变量,循环和条件语句)。

This is a guide to creating your first Flutter app. If you are familiar with object-oriented code and basic programming concepts such as variables, loops, and conditionals, you can complete this tutorial. You don’t need previous experience with Dart, mobile, or web programming.

完整的教程分为两部分,本页面是第一部分的内容,你可以在这里查看 第二部分 的内容。 (Codelabs 里的第一部分内容与本页内容相同)。

This codelab is part 1 of a two-part codelab. You can find part 2 on Google Developers Codelabs (as well as a copy of this codelab, part 1).

第一部分的内容概览

What you’ll build in part 1

你将完成一个简单的应用,功能是:为一个创业公司生成建议的公司名称。用户可以选择和取消选择的名称、保存喜欢的名称。该代码一次生成十个名称,当用户滚动时,会生成新一批名称。

You’ll implement a simple app that generates proposed names for a startup company. The user can select and unselect names, saving the best ones. The code lazily generates 10 names at a time. As the user scrolls, more names are generated. There is no limit to how far a user can scroll.

页面上方的这个 GIF 可以引导你预览本 codelab 做完之后的应用效果图。

The animated GIF shows how the app works at the completion of part 1.

任何一个 Flutter 的项目都可以编译为 web 应用。你可以在 IDE 中打开 devices 选择器,或者是在命令行中输入 flutter devices,这样你就可以看到 Chrome 以及 Web server 选项卡。 Chrome 设备将会自动打开 Chrome。 Web server 则会运行一个服务程式托管应用,这样你就可以在任意浏览器加载它。在开发时请使用 Chrome 进行调试,以使用 DevTools。当你想在其他浏览器测试时,请使用 web server。更多详细信息请查看 使用 Flutter 构建 web 应用,以及 编写你的第一个 Flutter web 应用程序

Every Flutter app you create also compiles for the web. In your IDE under the devices pulldown, or at the command line using flutter devices, you should now see Chrome and Web server listed. The Chrome device automatically starts Chrome. The Web server starts a server that hosts the app so that you can load it from any browser. Use the Chrome device during development so that you can use DevTools, and the web server when you want to test on other browsers. For more information, see Building a web application with Flutter and Write your first Flutter app on the web.

第一步:创建初始化工程

Step 1: Create the starter Flutter app

按照 这个指南 中所描述的步骤,创建一个简单的、基于模板的 Flutter 工程,然后将项目命名为 startup_namer (而不是 myapp),接下来你将会修改这个工程来完成最终的 App。

Create a simple, templated Flutter app, using the instructions in Getting Started with your first Flutter app. Name the project startup_namer (instead of flutter_app).

在这个示例中,你将主要编辑 Dart 代码所在的 lib/main.dart 文件,

You’ll mostly edit lib/main.dart, where the Dart code lives.

  1. 替换 lib/main.dart
    删除 lib/main.dart 中的所有代码,然后替换为下面的代码,它将在屏幕的中心显示”Hello World”。

    Replace the contents of lib/main.dart.
    Delete all of the code from lib/main.dart. Replace with the following code, which displays “Hello World” in the center of the screen.

    lib/main.dart
    // Copyright 2018 The Flutter team. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Welcome to Flutter'),
            ),
            body: const Center(
              child: Text('Hello World'),
            ),
          ),
        );
      }
    }
  2. 运行 你的工程项目,根据不同的操作系统,你会看到如下运行结果界面:

    Run the app in the way your IDE describes. You should see either Android, iOS, or web output, depending on your device.

    Hello world app on Android
    Android
    Hello world app on iOS
    iOS

观察和分析

Observations

第二步:使用外部 package

Step 2: Use an external package

在这一步中,你将开始使用一个名为 english_words 的开源软件包,其中包含数千个最常用的英文单词以及一些实用功能。

In this step, you’ll start using an open-source package named english_words, which contains a few thousand of the most used English words plus some utility functions.

你可以在 pub.dev 上找到 english_words 软件包以及其他许多开源软件包。

You can find the english_words package, as well as many other open source packages, on pub.dev.

  1. pubspec.yaml 文件管理 Flutter 应用程序的 assets(资源,如图片、package等)。在pubspec.yaml 中,将 english_words(3.1.5 或更高版本)添加到依赖项列表,如下面高亮显示的行:

    The pubspec.yaml file manages the assets and dependencies for a Flutter app. In pubspec.yaml, add english_words (3.1.5 or higher) to the dependencies list:

    {step1_base → step2_use_package}/pubspec.yaml
    @@ -9,4 +9,5 @@
    9
    9
      dependencies:
    10
    10
      flutter:
    11
    11
      sdk: flutter
    12
    12
      cupertino_icons: ^1.0.2
    13
    + english_words: ^4.0.0
  2. 在 Android Studio 的编辑器视图中查看 pubspec.yaml 文件时,点击 Pub get 会将依赖包安装到你的项目。你应该会在控制台中看到以下内容:

    While viewing the pubspec.yaml file in Android Studio’s editor view, click Pub get. This pulls the package into your project. You should see the following in the console:

    $ flutter pub get
    Running "flutter pub get" in startup_namer...
    Process finished with exit code 0
    

    在执行 Pub get 命令时会自动生成一个名为 pubspec.lock 文件,这里包含了你依赖 packages 的名称和版本。

    Performing Pub get also auto-generates the pubspec.lock file with a list of all packages pulled into the project and their version numbers.

  3. lib/main.dart 中引入,如下所示:

    In lib/main.dart, import the new package:

    lib/main.dart
    import 'package:english_words/english_words.dart';
    import 'package:flutter/material.dart';

    在你输入时,Android Studio会为你提供有关库导入的建议。然后它将呈现灰色的导入字符串,让你知道导入的库截至目前尚未被使用。

    As you type, Android Studio gives you suggestions for libraries to import. It then renders the import string in gray, letting you know that the imported library is unused (so far).

  4. 接下来,我们使用 English words 包生成文本来替换字符串”Hello World”:

    Use the English words package to generate the text instead of using the string “Hello World”:

    {step1_base → step2_use_package}/lib/main.dart
    @@ -9,14 +10,15 @@
    9
    10
      class MyApp extends StatelessWidget {
    10
    11
      @override
    11
    12
      Widget build(BuildContext context) {
    13
    + final wordPair = WordPair.random();
    12
    14
      return MaterialApp(
    13
    15
      title: 'Welcome to Flutter',
    14
    16
      home: Scaffold(
    15
    17
      appBar: AppBar(
    16
    18
      title: const Text('Welcome to Flutter'),
    17
    19
      ),
    18
    - body: const Center(
    19
    - child: Text('Hello World'),
    20
    + body: Center(
    21
    + child: Text(wordPair.asPascalCase),
    20
    22
      ),
    21
    23
      ),
    22
    24
      );
  5. 如果应用程序正在运行,请使用热重载按钮 offline_bolt 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。这是因为单词对是在 build 方法内部生成的。每次 MaterialApp 需要渲染时或者在 Flutter Inspector 中切换平台时 build 都会运行。

    If the app is running, hot reload to update the running app. Each time you click hot reload, or save the project, you should see a different word pair, chosen at random, in the running app. This is because the word pairing is generated inside the build method, which is run each time the MaterialApp requires rendering, or when toggling the Platform in Flutter Inspector.

    App at completion of second step on Android
    Android
    App at completion of second step on iOS
    iOS

遇到问题?

Problems?

如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following links to get back on track.

第三步:添加一个 Stateful widget

Step 3: Add a Stateful widget

Stateless widgets 是不可变的,这意味着它们的属性不能改变 —— 所有的值都是 final。

Stateless widgets are immutable, meaning that their properties can’t change—all values are final.

Stateful widgets 持有的状态可能在 widget 生命周期中发生变化,实现一个 stateful widget 至少需要两个类: 1)一个 StatefulWidget 类;2)一个 State 类,StatefulWidget 类本身是不变的,但是 State 类在 widget 生命周期中始终存在。

Stateful widgets maintain state that might change during the lifetime of the widget. Implementing a stateful widget requires at least two classes: 1) a StatefulWidget class that creates an instance of 2) a State class. The StatefulWidget class is, itself, immutable and can be thrown away and regenerated, but the State class persists over the lifetime of the widget.

在这一步,你将添加一个 stateful widget(有状态的 widget)—— RandomWords,它会创建自己的状态类 —— _RandomWordsState,然后你需要将 RandomWords 内嵌到已有的无状态的 MyApp widget。

In this step, you’ll add a stateful widget, RandomWords, which creates its State class, _RandomWordsState. You’ll then use RandomWords as a child inside the existing MyApp stateless widget.

  1. 创建有状态 widget 的样板代码。
    lib/main.dart 中,将光标置于所有代码之后,输入 回车 几次另起新行。在 IDE 中,输入 stful,编辑器就会提示您是否要创建一个 Stateful widget。按回车键表示接受建议,随后就会出现两个类的样板代码,光标也会被定位在输入有状态 widget 的名称处。

    Create the boilerplate code for a stateful widget.
    In lib/main.dart, position your cursor after all of the code, enter Return a couple times to start on a fresh line. In your IDE, start typing stful. The editor asks if you want to create a Stateful widget. Press Return to accept. The boilerplate code for two classes appears, and the cursor is positioned for you to enter the name of your stateful widget.

  2. 输入 RandomWords 作为有状态 widget 的名称。
    RandomWords widget 的主要作用就是创建其对应的 State 类。

    输入 RandomWords 作为有有状态 widget 的名称后, IDE 会自动更新其对应的 State 类,并将其命名为 _RandomWordsState。默认情况下,State 类的名称带有下划线前缀。 Dart 语言中,给标识符加上下划线前缀可以 增强隐私性,并且这也是针对 State 对象推荐的最佳实践写法。

    IDE 也会自动将状态类继承自 State<RandomWords>,这表示专门用于 RandomWords 的通用 State 类。该应用程序的大多数逻辑都位于此处—它维护 RandomWords widget 的状态。该类会保存生成的单词对的列表,该列表随用户滚动而无限增长,在本实验的第 2 部分中,用户可以通过点击心形图标,添加或删除列表中收藏的单词对。

    这两个类现在都如下所示:

    Enter RandomWords as the name of your widget.
    The RandomWords widget does little else beside creating its State class.

    Once you’ve entered RandomWords as the name of the stateful widget, the IDE automatically updates the accompanying State class, naming it _RandomWordsState. By default, the name of the State class is prefixed with an underbar. Prefixing an identifier with an underscore enforces privacy in the Dart language and is a recommended best practice for State objects.

    The IDE also automatically updates the state class to extend State<RandomWords>, indicating that you’re using a generic State class specialized for use with RandomWords. Most of the app’s logic resides here—it maintains the state for the RandomWords widget. This class saves the list of generated word pairs, which grows infinitely as the user scrolls and, in part 2 of this lab, favorites word pairs as the user adds or removes them from the list by toggling the heart icon.

    Both classes now look as follows:

    class RandomWords extends StatefulWidget {
      @override
      _RandomWordsState createState() => _RandomWordsState();
    }
    
    class _RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    
  3. 更新 _RandomWordsState 中的 build() 方法:

    Update the build() method in _RandomWordsState:

    lib/main.dart (_RandomWordsState)
    class _RandomWordsState extends State<RandomWords> {
        @override
        Widget build(BuildContext context) {
          final wordPair = WordPair.random();
          return Text(wordPair.asPascalCase);
        }
      }
  4. 通过以下差异所示的更改,删除 MyApp 中单词生成的代码:

    Remove the word generation code from MyApp by making the changes shown in the following diff:

    {step2_use_package → step3_stateful_widget}/lib/main.dart
    @@ -10,7 +10,6 @@
    10
    10
      class MyApp extends StatelessWidget {
    11
    11
      @override
    12
    12
      Widget build(BuildContext context) {
    13
    - final wordPair = WordPair.random();
    14
    13
      return MaterialApp(
    15
    14
      title: 'Welcome to Flutter',
    16
    15
      home: Scaffold(
    @@ -18,8 +17,8 @@
    18
    17
      title: const Text('Welcome to Flutter'),
    19
    18
      ),
    20
    19
      body: Center(
    21
    - child: Text(wordPair.asPascalCase),
    20
    + child: RandomWords(),
    22
    21
      ),
    23
    22
      ),
    24
    23
      );
    25
    24
      }
  5. 重启应用。应用应该像之前一样运行,每次热重载或保存应用程序时都会显示一个单词对。

    Restart the app. The app should behave as before, displaying a word pairing each time you hot reload or save the app.

  6. 遇到问题?

    Problems?

    如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

    If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following link to get back on track.

第四步:创建一个无限滚动的 ListView

Step 4: Create an infinite scrolling ListView

在该步骤中,您会拓展 _RandomWordsState 以生成并显示单词对列表。随着用户滚动,列表(显示在 ListView widget 中)将无限增长。 ListViewbuilder 工厂构造函数使您可以按需延迟构建列表视图。

In this step, you’ll expand _RandomWordsState to generate and display a list of word pairings. As the user scrolls the list (displayed in a ListView widget) grows infinitely. ListView’s builder factory constructor allows you to build a list view lazily, on demand.

  1. _RandomWordsState 类中添加一个 _suggestions 列表以保存建议的单词对,同时,添加一个 _biggerFont 变量来增大字体大小。

    Add a _suggestions list to the _RandomWordsState class for saving suggested word pairings. Also, add a _biggerFont variable for making the font size larger.

    lib/main.dart
    class _RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18.0);
      // ···
    }

    接下来,我们将向 _RandomWordsState 类添加一个 _buildSuggestions() 方法,此方法构建显示建议单词对的 ListView

    Next, you’ll add a _buildSuggestions() function to the _RandomWordsState class. This method builds the ListView that displays the suggested word pairing.

    ListView 类提供了一个名为 itemBuilder 的 builder 属性,这是一个工厂匿名回调函数,接受两个参数 BuildContext 和行迭代器 i。迭代器从 0 开始,每调用一次该函数 i 就会自增,每次建议的单词对都会让其递增两次,一次是 ListTile,另一次是 Divider。它用于创建一个在用户滚动时候无限增长的列表。

    The ListView class provides a builder property, itemBuilder, that’s a factory builder and callback function specified as an anonymous function. Two parameters are passed to the function—the BuildContext, and the row iterator, i. The iterator begins at 0 and increments each time the function is called. It increments twice for every suggested word pairing: once for the ListTile, and once for the Divider. This model allows the suggested list to continue growing as the user scrolls.

  2. _RandomWordsState 类添加 _buildSuggestions() 方法,内容如下:

    Add a _buildSuggestions() function to the _RandomWordsState class:

    lib/main.dart (_buildSuggestions)
    Widget _buildSuggestions() {
      return ListView.builder(
          padding: const EdgeInsets.all(16.0),
          itemBuilder: /*1*/ (context, i) {
            if (i.isOdd) return const Divider(); /*2*/
    
            final index = i ~/ 2; /*3*/
            if (index >= _suggestions.length) {
              _suggestions.addAll(generateWordPairs().take(10)); /*4*/
            }
            return _buildRow(_suggestions[index]);
          });
    }
    1. 对于每个建议的单词对都会调用一次 itemBuilder,然后将单词对添加到 ListTile 行中。在偶数行,该函数会为单词对添加一个 ListTile row,在奇数行,该函数会添加一个分割线的 widget,来分隔相邻的词对。注意,在小屏幕上,分割线看起来可能比较吃力。

      The itemBuilder callback is called once per suggested word pairing, and places each suggestion into a ListTile row. For even rows, the function adds a ListTile row for the word pairing. For odd rows, the function adds a Divider widget to visually separate the entries. Note that the divider might be difficult to see on smaller devices.

    2. ListView 里的每一行之前,添加一个 1 像素高的分隔线 widget。

      Add a one-pixel-high divider widget before each row in the ListView.

    3. 语法 i ~/ 2 表示 i 除以 2,但返回值是整型(向下取整),比如 i 为:1, 2, 3, 4, 5 时,结果为 0, 1, 1, 2, 2,这个可以计算出 ListView 中减去分隔线后的实际单词对数量。

      The expression i ~/ 2 divides i by 2 and returns an integer result. For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2. This calculates the actual number of word pairings in the ListView, minus the divider widgets.

    4. 如果是建议列表中最后一个单词对,接着再生成 10 个单词对,然后添加到建议列表。

      If you’ve reached the end of the available word pairings, then generate 10 more and add them to the suggestions list.

    对于每一个单词对,_buildSuggestions() 都会调用一次 _buildRow()。这个函数在 ListTile 中显示每个新词对,这使你在下一步中可以生成更漂亮的显示行,详见本 codelab 的第二部分。

    The _buildSuggestions() function calls _buildRow() once per word pair. This function displays each new pair in a ListTile, which allows you to make the rows more attractive in the next step.

  3. _RandomWordsState 中添加 _buildRow() 函数 :

    Add a _buildRow() function to _RandomWordsState:

    lib/main.dart (_buildRow)
    Widget _buildRow(WordPair pair) {
      return ListTile(
        title: Text(
          pair.asPascalCase,
          style: _biggerFont,
        ),
      );
    }
  4. 更新 _RandomWordsStatebuild() 方法以使用 _buildSuggestions(),而不是直接调用单词生成库,代码更改后如下:(使用 Scaffold 类实现基础的 Material Design 布局)

    In the _RandomWordsState class, update the build() method to use _buildSuggestions(), rather than directly calling the word generation library. (Scaffold implements the basic Material Design visual layout.) Replace the method body with the highlighted code:

    lib/main.dart (build)
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Startup Name Generator'),
        ),
        body: _buildSuggestions(),
      );
    }
  5. 更新 MyAppbuild() 方法,修改 title 的值来改变标题,修改 home 的值为 RandomWords widget。

    In the MyApp class, update the build() method by changing the title, and changing the home to be a RandomWords widget:

    {step3_stateful_widget → step4_infinite_list}/lib/main.dart
    @@ -10,15 +10,8 @@
    10
    10
      class MyApp extends StatelessWidget {
    11
    11
      @override
    12
    12
      Widget build(BuildContext context) {
    13
    13
      return MaterialApp(
    14
    - title: 'Welcome to Flutter',
    15
    - home: Scaffold(
    16
    - appBar: AppBar(
    17
    - title: const Text('Welcome to Flutter'),
    18
    - ),
    19
    - body: Center(
    20
    - child: RandomWords(),
    21
    - ),
    22
    - ),
    14
    + title: 'Startup Name Generator',
    15
    + home: RandomWords(),
    23
    16
      );
    24
    17
      }
  6. 重新启动你的项目工程应用,你应该看到一个单词对列表。尽可能地向下滚动,你将继续看到新的单词对。

    Restart the app. You should see a list of word pairings no matter how far you scroll.

    App at completion of fourth step on Android
    Android
    App at completion of fourth step on iOS
    iOS

遇到问题?

Problems?

如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following link to get back on track.

以 profile 模式运行

Profile or release runs

截止目前文档所示内容,你的应用应该运行在调试 (debug) 模式中,这个模式意味着在更大的性能开销下实现了更快速的开发效率,比如热重载功能的启用,因此你可能要面临较差质量的动画效果。当你准备分析应用性能或要打包发布的时候,你可能需要 Flutter 的 profile 或者 release 构建,相关文档,请查阅文档: Flutter 的构建模式选择

So far you’ve been running your app in debug mode. Debug mode trades performance for useful developer features such as hot reload and step debugging. It’s not unexpected to see slow performance and janky animations in debug mode. Once you are ready to analyze performance or release your app, you’ll want to use Flutter’s “profile” or “release” build modes. For more details, see Flutter’s build modes.

下一步

Next steps

The app from part 2
The app from part 2

祝贺你!

Congratulations!

你已经完成了一个可以同时运行在 iOS 和 Android 平台的 Flutter 应用!同时收获了如下内容:

You’ve written an interactive Flutter app that runs on both iOS and Android. In this codelab, you’ve:

如果你想继续扩展你的应用,在这里进行 第二部分,你将会从以下方面修改你的应用:

If you would like to extend this app, proceed to part 2 on the Google Developers Codelabs site, where you add the following functionality:

了解更多

目录

通过下面的文档列表了解更多关于 Flutter 框架的内容:

Learn more about the Flutter framework from the following pages:

基础

Flutter basics

应用其他平台开发经验

Apply your existing knowledge

其他资源

Other resources

你还可以通过 邮件列表 与我们取得联系,我们非常乐意听到你的反馈!

Reach out to us at our mailing list. We’d love to hear from you!

Happy Fluttering!

给 Android 开发者的 Flutter 指南

目录

这篇文档旨在帮助 Android 开发者利用既有的 Android 知识来通过 Flutter 开发移动应用。如果你了解 Android 框架的基本知识,你就可以使用这篇文档作为 Flutter 开发的快速入门。

This document is meant for Android developers looking to apply their existing Android knowledge to build mobile apps with Flutter. If you understand the fundamentals of the Android framework then you can use this document as a jump start to Flutter development.

你的 Android 知识和技能对于 Flutter 开发是非常有用的,因为 Flutter 依赖于 Android 操作系统的多种功能和配置。 Flutter 是一种全新的构建移动界面的方式,但是它有一套和 Android(以及 iOS)进行非 UI 任务通信的插件系统。如果你是一名 Android 专家,你就不必重新学习所有知识才能使用 Flutter。

Your Android knowledge and skill set are highly valuable when building with Flutter, because Flutter relies on the mobile operating system for numerous capabilities and configurations. Flutter is a new way to build UIs for mobile, but it has a plugin system to communicate with Android (and iOS) for non-UI tasks. If you’re an expert with Android, you don’t have to relearn everything to use Flutter.

这篇文档可以用作随时查阅以及答疑解惑的专题手册。

This document can be used as a cookbook by jumping around and finding questions that are most relevant to your needs.

视图 (Views)

Views

视图在 Flutter 中的对应概念是什么?

What is the equivalent of a View in Flutter?

Android 中的 View 是显示在屏幕上的一切的基础。按钮、工具栏、输入框以及一切内容都是 View。而 Flutter 中 View 的大致对应物是 Widget。 Widget 并非完全对应于 Android 中的 View,但是在你熟悉 Flutter 的工作原理的过程中可以把它们看做“声明和构建 UI 的方式”。

In Android, the View is the foundation of everything that shows up on the screen. Buttons, toolbars, and inputs, everything is a View. In Flutter, the rough equivalent to a View is a Widget. Widgets don’t map exactly to Android views, but while you’re getting acquainted with how Flutter works you can think of them as “the way you declare and construct UI”.

然而,widget 和 View 还是有一些差异。首先,widget 有着不一样的生命周期:它们是不可变的,一旦需要变化则生命周期终止。任何时候 widget 或它们的状态变化时, Flutter 框架都会创建一个新的 widget 树的实例。对比来看,一个 Android View 只会绘制一次,除非调用 invalidate 才会重绘。

However, these have a few differences to a View. To start, widgets have a different lifespan: they are immutable and only exist until they need to be changed. Whenever widgets or their state change, Flutter’s framework creates a new tree of widget instances. In comparison, an Android view is drawn once and does not redraw until invalidate is called.

Flutter 的 widget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象的语义的描述。

Flutter’s widgets are lightweight, in part due to their immutability. Because they aren’t views themselves, and aren’t directly drawing anything, but rather are a description of the UI and its semantics that get “inflated” into actual view objects under the hood.

Flutter 支持 Material Components 库。它提供实现了 Material Design 设计规范 的 widgets。 Material Design 是一套 为所有平台优化 (包括 iOS)的灵活的设计系统。

Flutter includes the Material Components library. These are widgets that implement the Material Design guidelines. Material Design is a flexible design system optimized for all platforms, including iOS.

Flutter 非常灵活、有表达能力,它可以实现任何设计语言。例如,在 iOS 平台上,你可以使用 Cupertino widgets 创建 Apple 的 iOS 设计语言 风格的界面。

But Flutter is flexible and expressive enough to implement any design language. For example, on iOS, you can use the Cupertino widgets to produce an interface that looks like Apple’s iOS design language.

如何更新 widgets?

How do I update widgets?

在 Android 中,你可以直接操作更新 View。然而在 Flutter 中, Widget 是不可变的,无法被直接更新,你需要操作 Widget 的状态。

In Android, you update your views by directly mutating them. However, in Flutter, Widgets are immutable and are not updated directly, instead you have to work with the widget’s state.

这就是有状态 (Stateful) 和无状态 (Stateless) Widget 概念的来源。 StatelessWidget 如其字面意思—没有状态信息的 Widget。

This is where the concept of Stateful and Stateless widgets comes from. A StatelessWidget is just what it sounds like—a widget with no state information.

StatelessWidget 用于你描述的用户界面的一部分不依赖于除了对象中的配置信息以外的任何东西的场景。

StatelessWidgets are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object.

例如在 Android 中,这就像显示一个展示图标的 ImageView。这个图标在运行过程中不会改变,所以在 Flutter 中就使用 StatelessWidget

For example, in Android, this is similar to placing an ImageView with your logo. The logo is not going to change during runtime, so use a StatelessWidget in Flutter.

如果你想要根据 HTTP 请求返回的数据或者用户的交互来动态地更新界面,那么你就必须使用 StatefulWidget,并告诉 Flutter 框架 Widget 的状态 (State) 更新了,以便 Flutter 可以更新这个 Widget。

If you want to dynamically change the UI based on data received after making an HTTP call or user interaction then you have to work with StatefulWidget and tell the Flutter framework that the widget’s State has been updated so it can update that widget.

这里需要着重注意的是,无状态和有状态的 Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget 有一个跨帧存储和恢复状态数据的 State 对象。

The important thing to note here is at the core both stateless and stateful widgets behave the same. They rebuild every frame, the difference is the StatefulWidget has a State object that stores state data across frames and restores it.

如果你有疑问,那么记住这条规则:如果一个 Widget 会变化(例如由于用户交互),它是有状态的。然而,如果一个 Widget 响应变化,它的父 Widget 只要本身不响应变化,就依然是无状态的。

If you are in doubt, then always remember this rule: if a widget changes (because of user interactions, for example) it’s stateful. However, if a widget reacts to change, the containing parent widget can still be stateless if it doesn’t itself react to change.

下面的例子展示了如何使用 StatelessWidgetText Widget 是一个普通的 StatelessWidget。如果你查看 Text Widget 的实现,你会发现它继承自 StatelessWidget

The following example shows how to use a StatelessWidget. A common StatelessWidget is the Text widget. If you look at the implementation of the Text widget you’ll find that it subclasses StatelessWidget.

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如上所示,这个 Text Widget 没有相关联的状态信息,它只渲染传入构造器的信息,仅此而已。

As you can see, the Text Widget has no state information associated with it, it renders what is passed in its constructors and nothing more.

但是,假如你想要动态地改变 “I Like Flutter”,例如当你点击一个 FloatingActionButton 的时候,该怎么办呢?

But, what if you want to make “I Like Flutter” change dynamically, for example when clicking a FloatingActionButton?

为了实现这个效果,将 Text Widget 嵌入一个 StatefulWidget 中,并在用户点击按钮的时候更新它。

To achieve this, wrap the Text widget in a StatefulWidget and update it when the user clicks the button.

例如:

For example:

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text.
  String textToShow = 'I Like Flutter';

  void _updateText() {
    setState(() {
      // Update the text.
      textToShow = 'Flutter is Awesome!';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

如何布局 Widget?我的 XML 布局文件在哪里?

How do I lay out my widgets? Where is my XML layout file?

在 Android 中,你通过 XML 文件定义布局,但是在 Flutter 中,你是通过一个 widget 树来定义布局的。

In Android, you write layouts in XML, but in Flutter you write your layouts with a widget tree.

以下示例展示了如何显示一个带有填充 (padding) 的简单 Widget:

The following example shows how to display a simple widget with padding:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            padding: EdgeInsets.only(left: 20.0, right: 30.0),
          ),
          onPressed: () {},
          child: Text('Hello'),
        ),
      ),
    );
  }

你可以在 widget 目录 中查看 Flutter 提供的布局。

You can view some of the layouts that Flutter has to offer in the widget catalog.

如何在布局中添加或删除一个组件?

How do I add or remove a component from my layout?

在 Android 中,你通过调用父 View 的 addChild()removeChild() 方法动态地添加或者删除子 View。在 Flutter 中,由于 Widget 是不可变的,所以没有 addChild() 的直接对应的方法。不过,你可以给返回一个 Widget 的父 Widget 传入一个方法,并通过布尔标记值控制子 Widget 的创建。

In Android, you call addChild() or removeChild() on a parent to dynamically add or remove child views. In Flutter, because widgets are immutable there is no direct equivalent to addChild(). Instead, you can pass a function to the parent that returns a widget, and control that child’s creation with a boolean flag.

例如,下面就是你可以如何在点击一个 FloatingActionButton 的时候在两个 Widget 之间切换。

For example, here is how you can toggle between two widgets when you click on a FloatingActionButton:

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle.
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return ElevatedButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

Widget 如何实现动画?

How do I animate a widget?

在 Android 中,你既可以通过 XML 文件定义动画,也可以调用 View 对象的 animate() 方法。在 Flutter 里,则使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。

In Android, you either create animations using XML, or call the animate() method on a view. In Flutter, animate widgets using the animation library by wrapping widgets inside an animated widget.

Flutter 通过 Animation<double> 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后你就可以创建一个或多个 Animation,并将它们绑定到控制器上。

In Flutter, use an AnimationController which is an Animation<double> that can pause, seek, stop and reverse the animation. It requires a Ticker that signals when vsync happens, and produces a linear interpolation between 0 and 1 on each frame while it’s running. You then create one or more Animations and attach them to the controller.

例如,你可以使用 CurvedAnimation 来实现一个曲线插值的动画。在这种情况下,控制器决定了动画进度,CurvedAnimation 计算用于替换控制器默认线性动画的曲线值。和 Widget 一样,Flutter 中的动画效果也可以组合使用。

For example, you might use CurvedAnimation to implement an animation along an interpolated curve. In this sense, the controller is the “master” source of the animation progress and the CurvedAnimation computes the curve that replaces the controller’s default linear motion. Like widgets, animations in Flutter work with composition.

在构建 Widget 树的时候,你需要将 Animation 对象赋值给某个 Widget 的动画属性,例如 FadeTransition 的不透明度属性,并让控制器开始动画。

When building the widget tree you assign the Animation to an animated property of a widget, such as the opacity of a FadeTransition, and tell the controller to start the animation.

下面的例子展示了如何实现一个点击 FloatingActionButton 的时候将一个 Widget 渐变为一个图标的 FadeTransition

The following example shows how to write a FadeTransition that fades the widget into a logo when you press the FloatingActionButton:

import 'package:flutter/material.dart';

void main() {
  runApp(FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  late AnimationController controller;
  late CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FadeTransition(
          opacity: curve,
          child: FlutterLogo(
            size: 100.0,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        onPressed: () {
          controller.forward();
        },
        child: Icon(Icons.brush),
      ),
    );
  }
}

获取更多内容,请查看 动画 Widget动画指南 以及 动画概览

For more information, see Animation & Motion widgets, the Animations tutorial, and the Animations overview.

如何使用 Canvas 进行绘制?

How do I use a Canvas to draw/paint?

在 Android 中,你可以使用 CanvasDrawable 将图片和形状绘制到屏幕上。Flutter 也有一个类似于 Canvas 的 API,因为它基于相同的底层渲染引擎 Skia。因此,在 Flutter 中用画布 (canvas) 进行绘制对于 Android 开发者来说是一件非常熟悉的工作。

In Android, you would use the Canvas and Drawables to draw images and shapes to the screen. Flutter has a similar Canvas API as well, since it is based on the same low-level rendering engine, Skia. As a result, painting to a canvas in Flutter is a very familiar task for Android developers.

Flutter 有两个帮助你用画布 (canvas) 进行绘制的类:CustomPaintCustomPainter,后者可以实现自定义的绘制算法。

Flutter has two classes that help you draw to the canvas: CustomPaint and CustomPainter, the latter of which implements your algorithm to draw to the canvas.

如果想学习在 Flutter 中如何实现一个签名功能,可以查看 Collin 在 Custom Paint 上的回答。

To learn how to implement a signature painter in Flutter, see Collin’s answer on Custom Paint.

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox? referenceBox = context.findRenderObject() as RenderBox;
          Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset?> points;
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i]!, points[i + 1]!, paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

如何创建自定义 Widget?

How do I build custom widgets?

在 Android 中,一般通过继承 View 类,或者使用已有的视图类,再覆写或实现可以达到特定效果的方法。

In Android, you typically subclass View, or use a pre-existing view, to override and implement methods that achieve the desired behavior.

在 Flutter 中,通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。这和 Android 中实现一个自定义的 ViewGroup 有些类似,所有的构建 UI 的模块代码都在手边,不过由你提供不同的行为—例如,自定义布局 (layout) 逻辑。

In Flutter, build a custom widget by composing smaller widgets (instead of extending them). It is somewhat similar to implementing a custom ViewGroup in Android, where all the building blocks are already existing, but you provide a different behavior—for example, custom layout logic.

举例来说,你该如何创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承 RaisedButton

For example, how do you build a CustomButton that takes a label in the constructor? Create a CustomButton that composes a ElevatedButton with a label, rather than by extending ElevatedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: Text(label),
    );
  }
}

然后就像使用其它 Flutter Widget 一样使用 CustomButton

Then use CustomButton, just as you’d use any other Flutter widget:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Intents

Intent 在 Flutter 中的对应概念是什么?

What is the equivalent of an Intent in Flutter?

在 Android 中,Intent 主要有两个使用场景:在 Activity 之前进行导航,以及组件间通信。 Flutter 却没有 intent 这样的概念,但是你依然可以通过原生集成 (插件) 来启动 intent。

In Android, there are two main use cases for Intents: navigating between Activities, and communicating with components. Flutter, on the other hand, does not have the concept of intents, although you can still start intents through native integrations (using a plugin).

Flutter 实际上并没有 Activity 和 Fragment 的对应概念。在 Flutter 中你需要使用 NavigatorRoute 在同一个 Activity 内的不同界面间进行跳转。

Flutter doesn’t really have a direct equivalent to activities and fragments; rather, in Flutter you navigate between screens, using a Navigator and Routes, all within the same Activity.

Route 是应用内屏幕和页面的抽象,Navigator 是管理路径 route 的工具。一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。Navigator 可以通过对 route 进行压栈和弹栈操作实现页面的跳转。Navigator 的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push()),想要返回的时候将 route 弹栈 (pop())。

A Route is an abstraction for a “screen” or “page” of an app, and a Navigator is a widget that manages routes. A route roughly maps to an Activity, but it does not carry the same meaning. A navigator can push and pop routes to move from screen to screen. Navigators work like a stack on which you can push() new routes you want to navigate to, and from which you can pop() routes when you want to “go back”.

在 Android 中,在应用的 AndroidManifest.xml 文件中声明 Activity。

In Android, you declare your activities inside the app’s AndroidManifest.xml.

在 Flutter 中,你有多种不同的方式在页面间导航:

In Flutter, you have a couple options to navigate between pages:

下面的例子创建了一个 Map。

The following example builds a Map.

void main() {
 runApp(MaterialApp(
   home: MyAppHome(), // Becomes the route named '/'.
   routes: <String, WidgetBuilder> {
     '/a': (BuildContext context) => MyPage(title: 'page A'),
     '/b': (BuildContext context) => MyPage(title: 'page B'),
     '/c': (BuildContext context) => MyPage(title: 'page C'),
   },
 ));
}

通过将 route 名压栈 (push) 到 Navigator 中来跳转到这个 route。

Navigate to a route by pushing its name to the Navigator.

Navigator.of(context).pushNamed('/b');

Intent 的另一种常见的使用场景是调用外部的组件,例如相机或文件选择器。对于这种情况,你需要创建一个原生平台插件(或者使用 已有的插件)。

The other popular use-case for Intents is to call external components such as a Camera or File picker. For this, you would need to create a native platform integration (or use an existing plugin).

想要学习如何创建一个原生平台集成,请查看 开发包和插件

To learn how to build a native platform integration, see developing packages and plugins.

在 Flutter 中应该如何处理从外部应用接收到的 intent?

How do I handle incoming intents from external applications in Flutter?

Flutter 可以通过直接和 Android 层通信并请求分享的数据来处理接收到的 Android intent。

Flutter can handle incoming intents from Android by directly talking to the Android layer and requesting the data that was shared.

下面的例子中,运行 Flutter 代码的原生 Activity 注册了一个文本分享的 intent 过滤器,这样其它应用就可以和 Flutter 应用分享文本了。

The following example registers a text share intent filter on the native activity that runs our Flutter code, so other apps can share text with our Flutter app.

从以上流程可以得知,我们首先在 Android 原生层面(在我们的 Activity 中)处理分享的文本数据,然后 Flutter 再通过使用 MethodChannel 获取这个数据。

The basic flow implies that we first handle the shared text data on the Android native side (in our Activity), and then wait until Flutter requests for the data to provide it using a MethodChannel.

首先,在 AndroidManifest.xml 中注册 intent 过滤器:

First, register the intent filter for all intents in AndroidManifest.xml:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

接着在 MainActivity 中处理 intent,提取出其它 intent 分享的文本并保存。当 Flutter 准备好处理的时候,它会使用一个平台通道请求数据,数据便会从原生端发送过来:

Then in MainActivity, handle the intent, extract the text that was shared from the intent, and hold onto it. When Flutter is ready to process, it requests the data using a platform channel, and it’s sent across from the native side:

package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.NonNull;

import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {

  private String sharedText;
  private static final String CHANNEL = "app.channel.shared.data";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine);

      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
              .setMethodCallHandler(
                      (call, result) -> {
                          if (call.method.contentEquals("getSharedText")) {
                              result.success(sharedText);
                              sharedText = null;
                          }
                      }
              );
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

最后,当 Widget 渲染的时候,从 Flutter 这端请求数据:

Finally, request the data from the Flutter side when the widget is rendered:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample Shared App Handler',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  static const platform = MethodChannel('app.channel.shared.data');
  String dataShared = 'No data';

  @override
  void initState() {
    super.initState();
    getSharedText();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }

  void getSharedText() async {
    var sharedData = await platform.invokeMethod('getSharedText');
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

startActivityForResult() 的对应方法是什么?

What is the equivalent of startActivityForResult()?

Navigator 类负责 Flutter 的导航,并用来接收被压栈的 route 的返回值。这是通过在 push() 后返回的 Futureawait 来实现的。

The Navigator class handles routing in Flutter and is used to get a result back from a route that you have pushed on the stack. This is done by awaiting on the Future returned by push().

例如,要打开一个让用户选择位置的 route,你可以这样做:

For example, to start a location route that lets the user select their location, you could do the following:

Map coordinates = await Navigator.of(context).pushNamed('/location');

然后,在你的位置 route 内,一旦用户选择了位置,你就可以弹栈 (pop) 并返回结果:

And then, inside your location route, once the user has selected their location you can pop the stack with the result:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

异步 UI

Async UI

runOnUiThread() 在 Flutter 中的对应方法是什么?

What is the equivalent of runOnUiThread() in Flutter?

Dart 有一个单线程执行的模型,同时也支持 Isolate (在另一个线程运行 Dart 代码的方法),它是一个事件循环和异步编程方式。除非你创建一个 Isolate,否则你的 Dart 代码会运行在主 UI 线程,并被一个事件循环所驱动。Flutter 的事件循环对应于 Android 里的主 Looper— 也即绑定到主线程上的 Looper

Dart has a single-threaded execution model, with support for Isolates (a way to run Dart code on another thread), an event loop, and asynchronous programming. Unless you spawn an Isolate, your Dart code runs in the main UI thread and is driven by an event loop. Flutter’s event loop is equivalent to Android’s main Looper—that is, the Looper that is attached to the main thread.

Dart 的单线程模型并不意味着你需要以会导致 UI 冻结的阻塞操作的方式来运行所有代码。不同于 Android 中需要你时刻保持主线程空闲,在 Flutter 中,可以使用 Dart 语言提供的异步工具,例如 async/await 来执行异步任务。如果你使用过 C# 或者 Javascript 中的 async/await 范式,或者 Kotlin 中的协程,你应该对它比较熟悉。

Dart’s single-threaded model doesn’t mean you need to run everything as a blocking operation that causes the UI to freeze. Unlike Android, which requires you to keep the main thread free at all times, in Flutter, use the asynchronous facilities that the Dart language provides, such as async/await, to perform asynchronous work. You might be familiar with the async/await paradigm if you’ve used it in C#, Javascript, or if you have used Kotlin’s coroutines.

例如,你可以通过使用 async/await 来运行网络代码而且不会导致 UI 挂起,同时让 Dart 来处理背后的繁重细节:

For example, you can run network code without causing the UI to hang by using async/await and letting Dart do the heavy lifting:

Future<void> loadData() async {
  var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

一旦用 await 修饰的网络操作完成,再调用 setState() 更新 UI,这会触发 widget 子树的重建并更新数据。

Once the awaited network call is done, update the UI by calling setState(), which triggers a rebuild of the widget sub-tree and updates the data.

下面的例子展示了异步加载数据并将之展示在 ListView 内:

The following example loads data asynchronously and displays it in a ListView:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  Future<void> loadData() async {
    var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

参考下一节内容获取更多关于后台任务以及 Flutter 与 Android 的差异的信息。

Refer to the next section for more information on doing work in the background, and how Flutter differs from Android.

如何将任务转移到后台线程?

How do you move work to a background thread?

在 Android 中,当你想要访问一个网络资源却又不想阻塞主线程并避免 ANR 的时候,你一般会将任务放到一个后台线程中运行。例如,你可以使用一个 AsyncTask、一个 LiveData、一个 IntentService、一个 JobScheduler 任务或者通过 RxJava 的管道用调度器将任务切换到后台线程中。

In Android, when you want to access a network resource you would typically move to a background thread and do the work, as to not block the main thread, and avoid ANRs. For example, you might be using an AsyncTask, a LiveData, an IntentService, a JobScheduler job, or an RxJava pipeline with a scheduler that works on background threads.

由于 Flutter 是单线程并且运行一个事件循环(类似 Node.js),你无须担心线程的管理以及后台线程的创建。如果你在执行和 I/O 绑定的任务,例如存储访问或者网络请求,那么你可以安全地使用 async/await,并无后顾之忧。再例如,你需要执行消耗 CPU 的计算密集型工作,那么你可以将其转移到一个 Isolate 上以避免阻塞事件循环,就像你在 Android 中会将任何任务放到主线程之外一样。

Since Flutter is single threaded and runs an event loop (like Node.js), you don’t have to worry about thread management or spawning background threads. If you’re doing I/O-bound work, such as disk access or a network call, then you can safely use async/await and you’re all set. If, on the other hand, you need to do computationally intensive work that keeps the CPU busy, you want to move it to an Isolate to avoid blocking the event loop, like you would keep any sort of work out of the main thread in Android.

对于和 I/O 绑定的任务,将方法声明为 async 方法,并在方法内 await 一个长时间运行的任务:

For I/O-bound work, declare the function as an async function, and await on long-running tasks inside the function:

Future<void> loadData() async {
  var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

这就是你一般应该如何执行网络和数据库操作,它们都属于 I/O 操作。

This is how you would typically do network or database calls, which are both I/O operations.

在 Android 中,当你继承 AsyncTask 的时候,你一般会覆写三个方法: onPreExecute()doInBackground()onPostExecute()。 Flutter 中没有对应的 API,你只需要 await 一个耗时方法调用, Dart 的事件循环就会帮你处理剩下的事情。

On Android, when you extend AsyncTask, you typically override 3 methods, onPreExecute(), doInBackground() and onPostExecute(). There is no equivalent in Flutter, since you await on a long running function, and Dart’s event loop takes care of the rest.

然而,有时候你可能需要处理大量的数据并挂起你的 UI。在 Flutter 中,可以通过使用 Isolate 来利用多核处理器的优势执行耗时或计算密集的任务。

However, there are times when you might be processing a large amount of data and your UI hangs. In Flutter, use Isolates to take advantage of multiple CPU cores to do long-running or computationally intensive tasks.

Isolate 是独立执行的线程,不会和主执行内存堆分享内存。这意味着你无法访问主线程的变量,或者调用 setState() 更新 UI。不同于 Android 中的线程,Isolate 如其名所示,它们无法分享内存(例如通过静态变量的形式)。

Isolates are separate execution threads that do not share any memory with the main execution memory heap. This means you can’t access variables from the main thread, or update your UI by calling setState(). Unlike Android threads, Isolates are true to their name, and cannot share memory (in the form of static fields, for example).

下面的例子展示了一个简单的 Isolate 是如何将数据分享给主线程来更新 UI 的。

The following example shows, in a simple isolate, how to share data back to the main thread to update the UI.

Future<void> loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message.
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(
    sendPort,
    "https://jsonplaceholder.typicode.com/posts",
  );

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

这里的 dataLoader() 就是运行在自己独立执行线程内的 Isolate。在 Isolate 中你可以执行更多的 CPU 密集型操作(例如解析一个大的 JSON 数据),或者执行计算密集型的数学运算,例如加密或信号处理。

Here, dataLoader() is the Isolate that runs in its own separate execution thread. In the isolate you can perform more CPU intensive processing (parsing a big JSON, for example), or perform computationally intensive math, such as encryption or signal processing.

你可以运行下面这个完整的例子:

You can run the full example below:

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  Widget getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: getBody(),
    );
  }

  ListView getListView() {
    return ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      },
    );
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  Future<void> loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message.
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(
      sendPort,
      'https://jsonplaceholder.typicode.com/posts',
    );

    setState(() {
      widgets = msg;
    });
  }

  // The entry point for the isolate.
  static Future<void> dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

OkHttp 在 Flutter 中的对应物是什么?

What is the equivalent of OkHttp on Flutter?

Flutter 中使用流行的 http 进行网络请求是很简单的。

Making a network call in Flutter is easy when you use the popular http package.

虽然 http 包没有 OkHttp 中的所有功能,但是它抽象了很多通常你会自己实现的网络功能,这使其本身在执行网络请求时简单易用。

While the http package doesn’t have every feature found in OkHttp, it abstracts away much of the networking that you would normally implement yourself, making it a simple way to make network calls.

如果要使用 http 包,需要在 pubspec.yaml 文件中添加依赖:

To use the http package, add it to your dependencies in pubspec.yaml:

dependencies:
  ...
  http: ^0.11.3+16

如果要发起一个网络请求,在异步 (async) 方法 http.get() 上调用 await 即可:

To make a network call, call await on the async function http.get():

import 'dart:convert';

import 'package:http/http.dart' as http;
// ...

Future<void> loadData() async {
  var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

如何为耗时任务显示进度?

How do I show the progress for a long-running task?

在 Android 中你通常会在后台执行一个耗时任务的时候显示一个 ProgressBar 在界面上。

In Android you would typically show a ProgressBar view in your UI while executing a long running task on a background thread.

在 Flutter 中,我们使用 ProgressIndicator widget。通过代码逻辑使用一个布尔标记值控制进度条的渲染。

In Flutter, use a ProgressIndicator widget. Show the progress programmatically by controlling when it’s rendered through a boolean flag. Tell Flutter to update its state before your long-running task starts, and hide it after it ends.

在下面的例子中,build 方法被拆分成三个不同的方法。如果 showLoadingDialog() 返回 true(当 widgets.length == 0),渲染 ProgressIndicator。否则,在 ListView 里渲染网络请求返回的数据。

In the following example, the build function is separated into three different functions. If showLoadingDialog is true (when widgets.isEmpty), then render the ProgressIndicator. Otherwise, render the ListView with the data returned from a network call.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  Widget getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: getBody(),
    );
  }

  ListView getListView() {
    return ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      },
    );
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  Future<void> loadData() async {
    var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

工程结构和资源文件

Project structure & resources

在哪里放置分辨率相关的图片文件?

Where do I store my resolution-dependent image files?

虽然 Android 区分对待资源文件 (resources) 和资产文件 (assets),但是 Flutter 应用只有资产文件 (assets)。所有原本在 Android 中应该放在 res/drawable-* 文件夹中的资源文件,在 Flutter 中都放在一个 assets 文件夹中。

While Android treats resources and assets as distinct items, Flutter apps have only assets. All resources that would live in the res/drawable-* folders on Android, are placed in an assets folder for Flutter.

Flutter 遵循一个简单的类似 iOS 的密度相关的格式。文件可以是一倍 (1.0x)、两倍 (2.0x)、三倍 (3.0x) 或其它的任意倍数。 Flutter 没有 dp 单位,但是有逻辑像素尺寸,基本和设备无关的像素尺寸是一样的。名称为 devicePixelRatio 的尺寸表示在单一逻辑像素标准下设备物理像素的比例。

Flutter follows a simple density-based format like iOS. Assets might be 1.0x, 2.0x, 3.0x, or any other multiplier. Flutter doesn’t have dps but there are logical pixels, which are basically the same as device-independent pixels. The so-called devicePixelRatio expresses the ratio of physical pixels in a single logical pixel.

和 Android 的密度分类的对照表如下:

The equivalent to Android’s density buckets are:

Android 密度修饰符

Android density qualifier

Flutter 像素比例

Flutter pixel ratio

ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

文件放置于任意文件夹中—Flutter 没有预先定义好的文件夹结构。你在 pubspec.yaml 文件中定义文件(包括位置信息),Flutter 负责找到它们。

Assets are located in any arbitrary folder—Flutter has no predefined folder structure. You declare the assets (with location) in the pubspec.yaml file, and Flutter picks them up.

需要注意的是,在 Flutter 1.0 beta 2 之前,在 Flutter 中定义的文件不能被原生端访问,反之亦然。原生端定义的资产文件 (assets) 和资源文件 (resources) 也无法被 Flutter 访问,因为它们是放置于不同的文件夹中的。

Note that before Flutter 1.0 beta 2, assets defined in Flutter were not accessible from the native side, and vice versa, native assets and resources weren’t available to Flutter, as they lived in separate folders.

至于 Flutter beta 2,文件是放置于原生端的 asset 文件夹中,所以可以被原生端的 AssetManager 访问:

As of Flutter beta 2, assets are stored in the native asset folder, and are accessed on the native side using Android’s AssetManager:

val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

然而对于 Flutter beta 2,Flutter 依然无法访问原生资源文件 (resources),也无法访问原生资产文件 (assets)。

As of Flutter beta 2, Flutter still cannot access native resources, nor it can access native assets.

如果你要向 Flutter 项目中添加一个新的叫 my_icon.png 的图片资源,并且将其放入我们随便起名的叫做 images 的文件夹中,你需要将基础图片(1.0x)放在 images 文件夹中,并将其它倍数的图片放入以特定倍数作为名称的子文件夹中:

To add a new image asset called my_icon.png to our Flutter project, for example, and deciding that it should live in a folder we arbitrarily called images, you would put the base image (1.0x) in the images folder, and all the other variants in sub-folders called with the appropriate ratio multiplier:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下来,你需要在 pubspec.yaml 文件中定义这些图片:

Next, you’ll need to declare these images in your pubspec.yaml file:

assets:
 - images/my_icon.jpeg

然后你就可以使用 AssetImage 访问你的图片了:

You can then access your images using AssetImage:

AssetImage('images/my_icon.jpeg');

或者通过 Image widget 直接访问:

or directly in an Image widget:

@override
Widget build(BuildContext context) {
  return Image.asset('images/my_image.png');
}

字符串储存在哪里?如何处理本地化?

Where do I store strings? How do I handle localization?

Flutter 当下并没有一个特定的管理字符串的资源管理系统。目前来讲,最好的办法是将字符串作为静态域存放在类中,并通过类访问它们。例如:

Flutter currently doesn’t have a dedicated resources-like system for strings. At the moment, the best practice is to hold your copy text in a class as static fields and accessing them from there. For example:

class Strings {
  static String welcomeMessage = 'Welcome To Flutter';
}

接着在你们的代码中,你可以这样访问你的字符串:

Then in your code, you can access your strings as such:

Text(Strings.welcomeMessage)

Flutter 在 Android 上提供无障碍的基本支持,但是这个功能当下仍在开发。

Flutter has basic support for accessibility on Android, though this feature is a work in progress.

我们鼓励 Flutter 开发者使用 intl 包 进行国际化和本地化。

Flutter developers are encouraged to use the intl package for internationalization and localization.

Gradle 文件的对应物是什么?我该如何添加依赖?

What is the equivalent of a Gradle file? How do I add dependencies?

在 Android 中,你在 Gradle 构建脚本中添加依赖。Flutter 使用 Dart 自己的构建系统以及 Pub 包管理器。构建工具会将原生 Android 和 iOS 壳应用的构建代理给对应的构建系统。

In Android, you add dependencies by adding to your Gradle build script. Flutter uses Dart’s own build system, and the Pub package manager. The tools delegate the building of the native Android and iOS wrapper apps to the respective build systems.

虽然在你的 Flutter 项目的 android 文件夹下有 Gradle 文件,但是它们只用于给对应平台的集成添加原生依赖。一般来说,在 pubspec.yaml 文件中定义在 Flutter 里使用的外部依赖。 pub.dev 是查找 Flutter packages 的好地方。

While there are Gradle files under the android folder in your Flutter project, only use these if you are adding native dependencies needed for per-platform integration. In general, use pubspec.yaml to declare external dependencies to use in Flutter. A good place to find Flutter packages is pub.dev.

Activity 和 Fragment

Activities and fragments

Activity 和 Fragment 在 Flutter 中的对应概念是什么?

What are the equivalent of activities and fragments in Flutter?

在 Android 中,一个 Activity 代表用户可以完成的一件独立任务。一个 Fragment 代表一个行为或者用户界面的一部分。 Fragment 用于模块化你的代码,为大屏组合复杂的用户界面,并适配应用的界面。在 Flutter 中,这两个概念都对应于 Widget

In Android, an Activity represents a single focused thing the user can do. A Fragment represents a behavior or a portion of user interface. Fragments are a way to modularize your code, compose sophisticated user interfaces for larger screens, and help scale your application UI. In Flutter, both of these concepts fall under the umbrella of Widgets.

如果要学习更多的关于 Activity 和 Fragment 创建界面的内容,请阅读社区贡献的 Medium 文章, 给 Android 开发者的 Flutter 指南:如何在 Flutter 中设计一个 Activity 界面

To learn more about the UI for building Activities and Fragements, see the community-contributed Medium article, Flutter for Android Developers: How to design Activity UI in Flutter.

就如在 Intents 部分所提,Flutter 中的界面都是以 Widget 表示的,因为 Flutter 中一切皆为 Widget。你使用 Navigator 在表示不同屏幕或页面,或者仅仅是相同数据的不同状态和渲染的各个 Route 之间进行导航。

As mentioned in the Intents section, screens in Flutter are represented by Widgets since everything is a widget in Flutter. Use a Navigator to move between different Routes that represent different screens or pages, or maybe just different states or renderings of the same data.

如何监听 Android Activity 的生命周期事件?

How do I listen to Android activity lifecycle events?

在 Android 中,你可以覆写 Actvity 的生命周期方法来监听其生命周期,也可以在 Application 上注册 ActivityLifecycleCallbacks。在 Flutter 中,这两种方法都没有,但是你可以通过绑定 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 的变化事件来监听生命周期。

In Android, you can override methods from the Activity to capture lifecycle methods for the activity itself, or register ActivityLifecycleCallbacks on the Application. In Flutter, you have neither concept, but you can instead listen to lifecycle events by hooking into the WidgetsBinding observer and listening to the didChangeAppLifecycleState() change event.

可以被观察的生命周期事件有:

The observable lifecycle events are:

想要了解这些状态含义的更多细节,请查看 AppLifecycleStatus 文档

For more details on the meaning of these states, see the AppLifecycleStatus documentation.

你可能已经注意到,只有一小部分的 Activity 生命周期事件是可用的;虽然 FlutterActivity 在内部捕获了几乎所有的 Activity 生命周期事件并将它们发送给 Flutter 引擎,但是它们大部分都向你屏蔽了。 Flutter 为你管理引擎的启动和停止,在大部分情况下几乎没有理由要在 Flutter 一端监听 Activity 的生命周期。如果你需要通过监听生命周期来获取或释放原生的资源,你无论如何都应该在原生一端做这件事。

As you might have noticed, only a small minority of the Activity lifecycle events are available; while FlutterActivity does capture almost all the activity lifecycle events internally and send them over to the Flutter engine, they’re mostly shielded away from you. Flutter takes care of starting and stopping the engine for you, and there is little reason for needing to observe the activity lifecycle on the Flutter side in most cases. If you need to observe the lifecycle to acquire or release any native resources, you should likely be doing it from the native side, at any rate.

下面的例子展示了如何监听容器 Activity 的生命周期状态:

Here’s an example of how to observe the lifecycle status of the containing activity:

import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null) {
      return Text(
        'This widget has not observed any lifecycle changes.',
        textDirection: TextDirection.ltr,
      );
    }

    return Text(
      'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
      textDirection: TextDirection.ltr,
    );
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

布局

Layouts

LinearLayout 的对应概念是什么?

What is the equivalent of a LinearLayout?

在 Android 中,LinearLayout 用于线性布局 widget 的—水平或者垂直。在 Flutter 中,使用 Row 或者 Column Widget 来实现相同的效果。

In Android, a LinearLayout is used to lay your widgets out linearly—either horizontally or vertically. In Flutter, use the Row or Column widgets to achieve the same result.

如果你注意看的话,会发现下面的两段代码除了 RowColumn widget 以外是一模一样的。它们的孩子是一样的,而这个特性可以被充分利用来开发包含有相同的孩子但是会随时间改变的复杂布局。

If you notice the two code samples are identical with the exception of the “Row” and “Column” widget. The children are the same and this feature can be exploited to develop rich layouts that can change overtime with the same children.

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('Row One'),
        Text('Row Two'),
        Text('Row Three'),
        Text('Row Four'),
      ],
    );
  }
@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );
}

如果想学习更多的构建线性布局的内容,请阅读社区贡献的 Medium 文章 给 Android 开发者的 Flutter 指南:如何在 Flutter 中设计线性布局?

To learn more about building linear layouts, see the community-contributed Medium article Flutter for Android Developers: How to design LinearLayout in Flutter.

RelativeLayout 的对应概念是什么?

What is the equivalent of a RelativeLayout?

RelativeLayout 通过 Widget 的相互位置对它们进行布局。在 Flutter 中,有几种实现相同效果的方法。

A RelativeLayout lays your widgets out relative to each other. In Flutter, there are a few ways to achieve the same result.

你可以通过组合使用 Column、Row 和 Stack Widget 实现 RelativeLayout 的效果。你还可以在 Widget 构造器内声明孩子相对父亲的布局规则。

You can achieve the result of a RelativeLayout by using a combination of Column, Row, and Stack widgets. You can specify rules for the widgets constructors on how the children are laid out relative to the parent.

Collin 在 StackOverflow 上的回答是一个在 Flutter 中构建相对布局的好例子。

For a good example of building a RelativeLayout in Flutter, see Collin’s answer on StackOverflow.

ScrollView 的对应概念是什么?

What is the equivalent of a ScrollView?

在 Android 中,使用 ScrollView 布局 widget—如果用户的设备屏幕比应用的内容区域小,用户可以滑动内容。

In Android, use a ScrollView to lay out your widgets—if the user’s device has a smaller screen than your content, it scrolls.

在 Flutter 中,实现这个功能的最简单的方法是使用 ListView widget。从 Android 的角度看,这样做可能是杀鸡用牛刀了,但是 Flutter 中 ListView widget 既是一个 ScrollView,也是一个 Android 中的 ListView。

In Flutter, the easiest way to do this is using the ListView widget. This might seem like overkill coming from Android, but in Flutter a ListView widget is both a ScrollView and an Android ListView.

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Text('Row One'),
        Text('Row Two'),
        Text('Row Three'),
        Text('Row Four'),
      ],
    );
  }

在 Flutter 中如何处理屏幕旋转?

How do I handle landscape transitions in Flutter?

FlutterView 会处理配置的变化,前提条件是在 AndroidManifest.xml 文件中声明了:

FlutterView handles the config change if AndroidManifest.xml contains:

android:configChanges="orientation|screenSize"

手势监听和触摸事件处理

Gesture detection and touch event handling

Flutter 中如何为一个 Widget 添加点击监听器?

How do I add an onClick listener to a widget in Flutter?

在 Android 中,你可以通过调用 setOnClickListener 方法在按钮这样的 View 上添加点击监听器。

In Android, you can attach onClick to views such as button by calling the method ‘setOnClickListener’.

在 Flutter 中有两种添加触摸监听器的方法:

In Flutter there are two ways of adding touch listeners:

  1. 如果 Widget 支持事件监听,那么向它传入一个方法并在方法中处理事件。例如,RaisedButton 有一个 onPressed 参数:

    If the Widget supports event detection, pass a function to it and handle it in the function. For example, the ElevatedButton has an onPressed parameter:

    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          print('click');
        },
        child: Text('Button'),
      );
    }
    
  2. 如果 Widget 不支持事件监听,将 Widget 包装进一个 GestureDetector 中并向 onTap 参数传入一个方法。

    If the Widget doesn’t support event detection, wrap the widget in a GestureDetector and pass a function to the onTap parameter.

    class SampleApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: GestureDetector(
              onTap: () {
                print('tap');
              },
              child: FlutterLogo(
                size: 200.0,
              ),
            ),
          ),
        );
      }
    }
    

如何处理 Widget 上的其它手势?

How do I handle other gestures on widgets?

使用 GestureDetector 可以监听非常多的手势,例如:

Using the GestureDetector, you can listen to a wide range of Gestures such as:

下面的例子展示了一个实现了双击旋转 Flutter 标志的 GestureDetector

The following example shows a GestureDetector that rotates the Flutter logo on a double tap:

AnimationController controller;
CurvedAnimation curve;

class SampleApp extends StatefulWidget {
  @override
  _SampleAppState createState() => _SampleAppState();
}

class _SampleAppState extends State<SampleApp> with SingleTickerProviderStateMixin {
  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 2000),
    );
    curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
          child: RotationTransition(
            turns: curve,
            child: FlutterLogo(
              size: 200.0,
            ),
          ),
        ),
      ),
    );
  }
}

Listviews 和 adapters

Listviews & adapters

ListView 在 Flutter 中的对应概念是什么?

What is the alternative to a ListView in Flutter?

Flutter 中 ListView 的对应概念仍然是…ListView!

The equivalent to a ListView in Flutter is … a ListView!

使用 Android 的 ListView 时,创建一个 adapter 并将其传给 ListView, ListView 渲染 adapter 返回的每一行内容。然后,你需要确保回收了每一行视图,否则,你会遇到各种奇怪的界面和内存问题。

In an Android ListView, you create an adapter and pass it into the ListView, which renders each row with what your adapter returns. However, you have to make sure you recycle your rows, otherwise, you get all sorts of crazy visual glitches and memory issues.

因为 Flutter widget 不可变的特点,你需要向 ListView 传入一组 widget, Flutter 会保证滑动的快速顺畅。

Due to Flutter’s immutable widget pattern, you pass a list of widgets to your ListView, and Flutter takes care of making sure that scrolling is fast and smooth.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(
        padding: EdgeInsets.all(10.0),
        child: Text('Row $i'),
      ));
    }
    return widgets;
  }
}

如何知道点击了哪个列表项?

How do I know which list item is clicked on?

在 Android 中,ListView 有一个可以帮助你定位哪个列表项被点击了的方法 onItemClickListener。在 Flutter 中,则使用传入 widget 的触摸监听。

In Android, the ListView has a method to find out which item was clicked ‘onItemClickListener’. In Flutter, use the touch handling provided by the passed-in widgets.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(
        GestureDetector(
          onTap: () {
            print('row tapped');
          },
          child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text('Row $i'),
          ),
        ),
      );
    }
    return widgets;
  }
}

如何动态更新 ListView?

How do I update ListView’s dynamically?

在 Android 中,你需要更新 adapter 并调用 notifyDataSetChanged

On Android, you update the adapter and call notifyDataSetChanged.

在 Flutter 中,如果你准备在 setState() 里更新一组 widget,你很快会发现你的数据并没有更新到界面上。这是因为当 setState() 被调用的时候, Flutter 渲染引擎会查看 Widget 树是否有任何更改。当引擎检查到 ListView,他会执行 == 检查,并判断两个 ListView 是一样的。没有任何更改,所以也就不需要更新。

In Flutter, if you were to update the list of widgets inside a setState(), you would quickly see that your data did not change visually. This is because when setState() is called, the Flutter rendering engine looks at the widget tree to see if anything has changed. When it gets to your ListView, it performs a == check, and determines that the two ListViews are the same. Nothing has changed, so no update is required.

更新 ListView 的一个简单方法是,在 setState() 里创建一个新的 List,并将数据从旧列表拷贝到新列表。虽然这个方法很简单,就如下面例子所示,但是并不推荐在大数据集的时候使用。

For a simple way to update your ListView, create a new List inside of setState(), and copy the data from the old list to the new list. While this approach is simple, it is not recommended for large data sets, as shown in the next example.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text('Row $i'),
      ),
    );
  }
}

推荐的高效且有效的创建一个列表的方法是使用 ListView.Builder。这个方法非常适用于动态列表或者拥有大量数据的列表。这基本上就是 Android 里的 RecyclerView,会为你自动回收列表项:

The recommended, efficient, and effective way to build a list uses a ListView.Builder. This method is great when you have a dynamic List or a List with very large amounts of data. This is essentially the equivalent of RecyclerView on Android, which automatically recycles list elements for you:

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text('Row $i'),
      ),
    );
  }
}

不用创建一个 “ListView”,而是创建接收两个参数的 ListView.Builder,两个参数分别是列表的初始长度和一个 ItemBuilder 方法。

Instead of creating a “ListView”, create a ListView.builder that takes two key parameters: the initial length of the list, and an ItemBuilder function.

ItemBuilder 方法和 Android adapter 里的 getView 方法类似;它通过位置返回你期望在这个位置渲染的列表项。

The ItemBuilder function is similar to the getView function in an Android adapter; it takes a position, and returns the row you want rendered at that position.

最后也是最重要的一条,需要注意 onTap() 方法不再重建列表项,但是会执行 .add 操作。

Finally, but most importantly, notice that the onTap() function doesn’t recreate the list anymore, but instead .adds to it.

文字处理

Working with text

如何为 Text Widget 设置自定义字体?

How do I set custom fonts on my Text widgets?

在 Android SDK 中(从 Android O 开始),你可以创建一个字体资源文件并将其传给 TextView 的 FontFamily 参数。

In Android SDK (as of Android O), you create a Font resource file and pass it into the FontFamily param for your TextView.

在 Flutter 中,将字体文件放入一个文件夹,并在 pubspec.yaml 文件中引用它,就和导入图片一样。

In Flutter, place the font file in a folder and reference it in the pubspec.yaml file, similar to how you import images.

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

然后将字体赋值给你的 Text Widget:

Then assign the font to your Text widget:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Sample App'),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何更改 Text Widget 的样式?

How do I style my Text widgets?

除了字体,你还可以自定义 Text Widget 的其它样式元素。Text Widget 的样式参数接收一个 TextStyle 对象,你可以在这个对象里自定义很多参数,例如:

Along with fonts, you can customize other styling elements on a Text widget. The style parameter of a Text widget takes a TextStyle object, where you can customize many parameters, such as:

表单输入

Form input

如果需要更多使用表单的信息,请查看 Flutter Cookbook 中的 检索一个文本字段的值

For more information on using Forms, see Retrieve the value of a text field, from the Flutter Cookbook.

Input 的“提示” (hint) 的对应概念是什么?

What is the equivalent of a “hint” on an Input?

在 Flutter 中,你可以简单地通过向 Text Widget 构造器的 decoration 参数传入一个 InputDecoration 对象来为输入框展示一个“提示”或占位文本。

In Flutter, you can easily show a “hint” or a placeholder text for your input by adding an InputDecoration object to the decoration constructor parameter for the Text Widget.

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: 'This is a hint'),
  )
)

如何显示验证错误的信息?

How do I show validation errors?

就像上面实现“提示”功能一样,像 Text Widget 构造方法的 decoration 参数传入一个 InputDecoration 对象。

Just as you would with a “hint”, pass an InputDecoration object to the decoration constructor for the Text widget.

然而,你并不想一开始就显示错误信息。相反,当用户输入了无效的信息后,更新状态并传入一个新的 InputDecoration 对象。

However, you don’t want to start off by showing an error. Instead, when the user has entered invalid data, update the state, and pass a new InputDecoration object.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key? key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(
            hintText: 'This is a hint',
            errorText: _getErrorText(),
          ),
        ),
      ),
    );
  }

  String _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

Flutter 插件

Flutter plugins

如何使用 GPS 传感器?

How do I access the GPS sensor?

使用 geolocator 社区插件。

Use the geolocator community plugin.

如何使用相机?

How do I access the camera?

image_picker 插件被常用于相机功能的使用。

The image_picker plugin is popular for accessing the camera.

如何使用 Facebook 登录?

How do I log in with Facebook?

使用 flutter_facebook_login 社区插件实现 Facebook 登录功能。

To Log in with Facebook, use the flutter_facebook_login community plugin.

如何使用 Firebase 的功能?

How do I use Firebase features?

官方插件 提供了 Firebase 的大多数功能。这些插件都是由 Flutter 团队维护的官方集成插件:

Most Firebase functions are covered by first party plugins. These plugins are first-party integrations, maintained by the Flutter team:

你可以在 Pub 网站上查找一些官方插件没有直接支持的功能的第三方 Firebase 插件。

You can also find some third-party Firebase plugins on Pub that cover areas not directly covered by the first-party plugins.

如何创建自己的自定义原生集成插件?

How do I build my own custom native integrations?

如果有 Flutter 官方或社区第三方插件没有涵盖的平台特定的功能,你可以根据 开发包和插件 页面创建自己的插件。

If there is platform-specific functionality that Flutter or its community Plugins are missing, you can build your own following the developing packages and plugins page.

Flutter 的插件架构,简而言之,和 Android 中的事件总线的使用非常相似:你发送一个消息,并让接受者处理并返回一个结果给你。在这种情况下,接受者是运行在 Android 或 iOS 原生端的代码。

Flutter’s plugin architecture, in a nutshell, is much like using an Event bus in Android: you fire off a message and let the receiver process and emit a result back to you. In this case, the receiver is code running on the native side on Android or iOS.

如何在 Flutter 应用中使用 NDK?

How do I use the NDK in my Flutter application?

如果你在现有的 Android 应用中使用 NDK,并且希望你的 Flutter 应用可以利用你的 native 库,这可以通过创建一个自定义插件实现。

If you use the NDK in your current Android application and want your Flutter application to take advantage of your native libraries then it’s possible by building a custom plugin.

你的自定义插件首先和你的 Android 应用通信,Android 应用会通过 JNI 调用 native 方法。一旦有返回值,就可以向 Flutter 发送回一个消息并渲染结果。

Your custom plugin first talks to your Android app, where you call your native functions over JNI. Once a response is ready, send a message back to Flutter and render the result.

暂时还不支持从 Flutter 中直接调用 native 代码。

Calling native code directly from Flutter is currently not supported.

主题(Themes)

Themes

如何对应用使用主题?

How do I theme my app?

Flutter 提供开箱即用的优美的 Material Design 实现,可以满足你通常需要的各种样式和主题的需求。不同于 Android 中你在 XML 文件中定义主题并在 AndroidManifest.xml 中将其赋值给你的应用, Flutter 中是在顶层 Widget 上声明主题。

Out of the box, Flutter comes with a beautiful implementation of Material Design, which takes care of a lot of styling and theming needs that you would typically do. Unlike Android where you declare themes in XML and then assign it to your application using AndroidManifest.xml, in Flutter you declare themes in the top level widget.

为了在应用中利用好 Material 组件,你可以在应用中声明一个顶层 Widget MeterialApp 作为入口。 MaterialApp 是一个包装了一系列 Widget 的为你给予便利的 Widget,而这些 Widget 通常是实现 Material Design 的应用所必须的。它基于 WidgetsApp 并添加了 Material 相关的功能。

To take full advantage of Material Components in your app, you can declare a top level widget MaterialApp as the entry point to your application. MaterialApp is a convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design. It builds upon a WidgetsApp by adding Material specific functionality.

你也可以使用 WidgetApp 作为应用的 Widget,它会提供一些相同的功能,但是不如 MaterialApp 提供的功能丰富。

You can also use a WidgetsApp as your app widget, which provides some of the same functionality, but is not as rich as MaterialApp.

如果要自定义任意子组件的颜色或者样式,给 MaterialApp Widget 传入一个 ThemeData 对象即可。例如,在下面的代码中,主色调设置为蓝色,文本选中颜色设置为红色。

To customize the colors and styles of any child components, pass a ThemeData object to the MaterialApp widget. For example, in the code below, the primary swatch is set to blue and text selection color is red.

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionTheme: TextSelectionThemeData(selectionColor: Colors.red),
      ),
      home: SampleAppPage(),
    );
  }
}

数据库和本地存储

Databases and local storage

如何使用 Shared Preferences?

How do I access Shared Preferences?

在 Android 中,你可以使用 SharedPreferences API 来存储少量的键值对。

In Android, you can store a small collection of key-value pairs using the SharedPreferences API.

在 Flutter 中,使用 Shared_Preferences 插件 实现此功能。这个插件同时包装了 Shared Preferences 和 NSUserDefaults(iOS 平台对应 API)的功能。

In Flutter, access this functionality using the Shared_Preferences plugin. This plugin wraps the functionality of both Shared Preferences and NSUserDefaults (the iOS equivalent).

import 'package:flutter/material.dart';

import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

void _incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  prefs.setInt('counter', counter);
}

在 Flutter 中如何使用 SQLite?

How do I access SQLite in Flutter?

在 Android 中,你会使用 SQLite 来存储可以通过 SQL 进行查询的结构化数据。

In Android, you use SQLite to store structured data that you can query using SQL.

在 Flutter 中,使用 SQFlite 插件实现此功能。

In Flutter, access this functionality using the SQFlite plugin.

Debugging

应该使用什么工具调试我的 Flutter 应用?

What tools can I use to debug my app in Flutter?

请使用 开发者工具 debug 你的 Flutter 和 Dart 应用。

Use the DevTools suite for debugging Flutter or Dart apps.

开发者工具包含了 profiling 构建、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 开发者工具 文档。

DevTools includes support for profiling, examining the heap, inspecting the widget tree, logging diagnostics, debugging, observing executed lines of code, debugging memory leaks and memory fragmentation. For more information, see the DevTools documentation.

通知

Notifications

如何设置推送通知?

How do I set up push notifications?

在 Android 中,你可以使用 Firebase Cloud Messaging 来为应用设置推送通知。

In Android, you use Firebase Cloud Messaging to setup push notifications for your app.

在 Flutter 中,则使用 Firebase Messaging 插件实现此功能。想要获得更多关于使用 Firebase Cloud Messaging API 的信息,请查阅 firebase_messaging 插件文档。

In Flutter, access this functionality using the Firebase Messaging plugin. For more information on using the Firebase Cloud Messaging API, see the firebase_messaging plugin documentation.

给 iOS 开发者的 Flutter 指南

目录

这篇文章是为那些想将已有的 iOS 开发经验运用到 Flutter 开发中的 iOS 开发者所作。如果你理解 iOS framework 的基本原理,那么你可以将这篇文章作为学习 Flutter 开发的起点。

This document is for iOS developers looking to apply their existing iOS knowledge to build mobile apps with Flutter. If you understand the fundamentals of the iOS framework then you can use this document as a way to get started learning Flutter development.

在开始本文档之前,建议先浏览一下这个 15 分钟的视频,了解一下 Cupertino package 是什么吧:

Before diving into this doc, you might want to watch a 15-minute video from the Flutter Youtube channel about the Cupertino package.

你的 iOS 开发技能对于开发 Flutter 而言非常宝贵,因为 Flutter 也依赖操作系统进行众多功能和配置。 Flutter 是一种全新的构建移动应用的方式,同时它也包含了可以与 iOS(和 Android)进行非 UI 通信的插件系统。如果你已经是 iOS 开发专家,那么你并不需要重新学习 Flutter 的所有内容。

Your iOS knowledge and skill set are highly valuable when building with Flutter, because Flutter relies on the mobile operating system for numerous capabilities and configurations. Flutter is a new way to build UIs for mobile, but it has a plugin system to communicate with iOS (and Android) for non-UI tasks. If you’re an expert in iOS development, you don’t have to relearn everything to use Flutter.

Flutter 早已对在 iOS 上运行 Flutter 框架作了许多优化。若你需要查看相关内容,请阅读 平台适配

Flutter also already makes a number of adaptations in the framework for you when running on iOS. For a list, see Platform adaptations.

你同样可以将这篇文章当作一份手册查看,以便查找并解决你所遇到的问题。

This document can be used as a cookbook by jumping around and finding questions that are most relevant to your needs.

视图

Views

UIView 相当于 Flutter 中的什么?

What is the equivalent of a UIView in Flutter?

在 iOS 中,你在 UI 中创建的大部分视图都是 UIView 的实例。而在构造布局时,这些视图也可以作为其他视图的容器。

On iOS, most of what you create in the UI is done using view objects, which are instances of the UIView class. These can act as containers for other UIView classes, which form your layout.

在 Flutter 中,同 UIView 能够进行类比的就是 Widget 了。但 Widget 和 iOS 里的视图并不能同等对待,不过当你想要了解 Flutter 的工作原理时,你可以把它理解为“声明和构造 UI 的方法”。

In Flutter, the rough equivalent to a UIView is a Widget. Widgets don’t map exactly to iOS views, but while you’re getting acquainted with how Flutter works you can think of them as “the way you declare and construct UI”.

然而,WidgetUIView 还是有着相当一部分区别的。首先,widget 拥有着不同的生命周期:整个生命周期内它是不可变的,且只能够存活到被修改的时候。一旦 widget 实例或者它的状态发生了改变, Flutter 框架就会创建一个新的由 Widget 实例构造而成的树状结构。而在 iOS 里,修改一个视图并不会导致它重新创建实例,它作为一个可变对象,只会绘制一次,只有在发生 setNeedsDisplay() 调用之后才会发生重绘。

However, these have a few differences to a UIView. To start, widgets have a different lifespan: they are immutable and only exist until they need to be changed. Whenever widgets or their state change, Flutter’s framework creates a new tree of widget instances. In comparison, an iOS view is not recreated when it changes, but rather it’s a mutable entity that is drawn once and doesn’t redraw until it is invalidated using setNeedsDisplay().

还有,和 UIView 不同,Flutter 的 widget 是很轻量的,一部分原因就是源于它的不可变特性。因为它并不是视图,也不直接绘制任何内容,而是作为对 UI 及其特性的一种描述,而被“注入”到视图中去。

Furthermore, unlike UIView, Flutter’s widgets are lightweight, in part due to their immutability. Because they aren’t views themselves, and aren’t directly drawing anything, but rather are a description of the UI and its semantics that get “inflated” into actual view objects under the hood.

Flutter 包含了 Material Components 库。内容都是一些遵循了 Material Design 设计规范 的组件。 Material Design 是一种灵活的 支持全平台 的设计体系,其中也包括了 iOS。

Flutter includes the Material Components library. These are widgets that implement the Material Design guidelines. Material Design is a flexible design system optimized for all platforms, including iOS.

但是 Flutter 的灵活性和表现力使其能够适配任何的设计语言。在 iOS 中,你可以通过 Cupertino widgets 来构造类似于 Apple iOS 设计语言的接口。

But Flutter is flexible and expressive enough to implement any design language. On iOS, you can use the Cupertino widgets to produce an interface that looks like Apple’s iOS design language.

我该如何更新 widget?

How do I update widgets?

在 iOS 可以直接对视图进行修改。但是在 Flutter 中,widget 都是不可变的,所以也不能够直接对其修改。所以,你必须通过修改 widget 的 state 来达到更新视图的目的。

To update your views on iOS, you directly mutate them. In Flutter, widgets are immutable and not updated directly. Instead, you have to manipulate the widget’s state.

于是,就引入了 Stateful widget 和 Stateless widget 的概念。和字面意思相同,StatelessWidget 就是一个没有绑定状态的 widget。

This is where the concept of Stateful vs Stateless widgets comes in. A StatelessWidget is just what it sounds like—a widget with no state attached.

当某个 widget 不需要依赖任何别的初始配置来对这个 widget 进行描述时, StatelessWidgets 会是很有用的。

StatelessWidgets are useful when the part of the user interface you are describing does not depend on anything other than the initial configuration information in the widget.

举个例子,在 iOS 中,你需要把 logo 当作 image 并将它放置在 UIImageView 中,如果在运行时这个 logo 不会发生变化,那么对应 Flutter 中你应该使用 StatelessWidget

For example, in iOS, this is similar to placing a UIImageView with your logo as the image. If the logo is not changing during runtime, use a StatelessWidget in Flutter.

但是如果你想要根据 HTTP 请求的返回结果动态的修改 UI,那么你应该使用 StatefulWidget。在 HTTP 请求结束后,通知 Flutter 更新这个 widget 的 State,然后 UI 就会得到更新。

If you want to dynamically change the UI based on data received after making an HTTP call, use a StatefulWidget. After the HTTP call has completed, tell the Flutter framework that the widget’s State is updated, so it can update the UI.

StatefulWidgetStatelessWidget 最重要的区别就是, StatefulWidget 中有一个 State 对象,它用来存储一些状态的信息,并在整个生命周期内保持不变。

The important difference between stateless and stateful widgets is that StatefulWidgets have a State object that stores state data and carries it over across tree rebuilds, so it’s not lost.

如果你对此还存有疑虑,记住一点:如果一个 widget 在 build 方法之外(比如运行时下发生用户点击事件)被修改,那么就应该是有状态的。如果一个 widget 一旦生成就不再发生改变,那么它就是无状态的。然而,即使一个 widget 是有状态的,如果不是自身直接响应修改(或别的输入),那么他的父容器也可以是无状态的。

If you are in doubt, remember this rule: if a widget changes outside of the build method (because of runtime user interactions, for example), it’s stateful. If the widget never changes, once built, it’s stateless. However, even if a widget is stateful, the containing parent widget can still be stateless if it isn’t itself reacting to those changes (or other inputs).

下面是如何使用 StatelessWidget 的示例。Text 是一个常用的 StatelessWidget。如果你看了 Text 的源代码,就会发现它继承于 StatelessWidget

The following example shows how to use a StatelessWidget. A common StatelessWidget is the Text widget. If you look at the implementation of the Text widget you’ll find it subclasses StatelessWidget.

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

看了上面的代码,你会注意到 Text 没有携带任何状态,它只会渲染初始化时传进来的内容。

If you look at the code above, you might notice that the Text widget carries no explicit state with it. It renders what is passed in its constructors and nothing more.

然而,如果你想要动态地修改文本为 “I Like Flutter”,比如说在点击一个 FloatingActionButton 时该怎么做呢?

But, what if you want to make “I Like Flutter” change dynamically, for example when clicking a FloatingActionButton?

想要实现这个需求,只需要把 Text 放到 StatefulWidget 中,并在用户点击按钮时更新它即可。

To achieve this, wrap the Text widget in a StatefulWidget and update it when the user clicks the button.

下面是示例代码:

For example:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";
  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

如何对 widget 做布局?Storyboard 哪去了?

How do I lay out my widgets? Where is my Storyboard?

在 iOS 开发中,你可能会经常使用 Storyboard 来组织你的视图,并直接通过 Storyboard 或者在 ViewController 中通过代码来设置约束。而在 Flutter 中,你要通过代码来对 widget 进行组织来形成一个 widget 树状结构。

In iOS, you might use a Storyboard file to organize your views and set constraints, or you might set your constraints programmatically in your view controllers. In Flutter, declare your layout in code by composing a widget tree.

下面的例子展示了如何展示一个带有 padding 的 widget:

The following example shows how to display a simple widget with padding:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: CupertinoButton(
        onPressed: () {
          setState(() { _pressedCount += 1; });
        },
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

你可以为任何 widget 添加 padding,来达到类似在 iOS 中视图约束的作用。

You can add padding to any widget, which mimics the functionality of constraints in iOS.

你可以在 widget 目录 中查看 Flutter 提供的所有 widget 布局方法。

You can view the layouts that Flutter has to offer in the widget catalog.

如何增加或者移除一个组件?

How do I add or remove a component from my layout?

在 iOS 中,你可以通过调用父视图的 addSubview() 方法或者 removeFromSuperview() 方法来动态的添加或移除视图。在 Flutter 中,因为 widget 是不可变的,所以没有提供直接同 addSubview() 作用相同的方法。但是你可以通过向父视图传递一个返回值是 widget 的方法,并通过一个 boolean flag 来控制子视图的存在。

In iOS, you call addSubview() on the parent, or removeFromSuperview() on a child view to dynamically add or remove child views. In Flutter, because widgets are immutable there is no direct equivalent to addSubview(). Instead, you can pass a function to the parent that returns a widget, and control that child’s creation with a boolean flag.

下面的例子中像你展示了如何让用户通过点击 FloatingActionButton 按钮来达到在两个 widget 中切换的目的。

The following example shows how to toggle between two widgets when the user clicks the FloatingActionButton:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

如何添加动画?

How do I animate a widget?

在 iOS 里,你可以使用调用视图的 animate(withDuration:animations:) 方法来创建动画。在 Flutter 里,通过使用动画库将 widget 封装到 animated widget 中来实现带动画效果。

In iOS, you create an animation by calling the animate(withDuration:animations:) method on a view. In Flutter, use the animation library to wrap widgets inside an animated widget.

在 Flutter 里,使用 AnimationController,它是一个可以暂停、查找、停止和反转动画的 Animation<double> 类型。它需要一个 Ticker,在屏幕刷新时发出信号量,并在运行时对每一帧都产生一个 0~1 的线性差值。然后你可以创建一个或多个 Animation,并把它们添加到控制器中。

In Flutter, use an AnimationController, which is an Animation<double> that can pause, seek, stop, and reverse the animation. It requires a Ticker that signals when vsync happens and produces a linear interpolation between 0 and 1 on each frame while it’s running. You then create one or more Animations and attach them to the controller.

比如,你可以使用 CurvedAnimation 来实现一个曲线翻页动画。这种情况下,控制器就是动画进度的主要数据源,而 CurvedAnimation 计算曲线并替换控制器的默认线性运动,和 widget 一样,在 Flutter 里动画也可以复合嵌套。

For example, you might use CurvedAnimation to implement an animation along an interpolated curve. In this sense, the controller is the “master” source of the animation progress and the CurvedAnimation computes the curve that replaces the controller’s default linear motion. Like widgets, animations in Flutter work with composition.

当构建一个 widget 树时,可以将 Animation 赋值给 widget 用户表现动画能力的属性,比如 FadeTransition 的 opacity 属性,然后告诉控制器启动动画。

When building the widget tree you assign the Animation to an animated property of a widget, such as the opacity of a FadeTransition, and tell the controller to start the animation.

下面的示例描述了当你点击 FloatingActionButton 时,如何实现一个视图渐淡出成 logo 的 FadeTransition 效果。

The following example shows how to write a FadeTransition that fades the widget into a logo when you press the FloatingActionButton:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          child: FadeTransition(
            opacity: curve,
            child: FlutterLogo(
              size: 100.0,
            )
          )
        )
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }

  @override
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

关于更多的内容,可以查看 Animation 和 Motion widgetsAnimations 教程,以及 Animations 概览

For more information, see Animation & Motion widgets, the Animations tutorial, and the Animations overview.

如何渲染到屏幕上?

How do I draw to the screen?

在 iOS 里,可以使用 CoreGraphics 绘制线条和图形到屏幕上。 Flutter 里有一套基于 Cavans 实现的 API,有两个类可以帮助你进行绘制: CustomPaintCustomPainter,后者实现了绘制图形到 canvas 的算法。

On iOS, you use CoreGraphics to draw lines and shapes to the screen. Flutter has a different API based on the Canvas class, with two other classes that help you draw: CustomPaint and CustomPainter, the latter of which implements your algorithm to draw to the canvas.

想要学习在 Flutter 里如何实现一个画笔,可以查看 Collin 在 StackOverflow 里的回答。

To learn how to implement a signature painter in Flutter, see Collin’s answer on StackOverflow.

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
              referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

如何设置视图 widget 的透明度?

Where is the widget’s opacity?

在 iOS 里,视图都有一个 opacity 或者 alpha 属性。而在 Flutter 里,大部分时候你都需要封装 widget 到一个 Opacity widget 中来实现这一功能。

On iOS, everything has .opacity or .alpha. In Flutter, most of the time you need to wrap a widget in an Opacity widget to accomplish this.

如何构建自定义 widget?

How do I build custom widgets?

在 iOS 里,你可以直接继承 UIView 或者使用已经存在的视图,然后重写并实现对应的方法来达到想要的效果。在 Flutter 里,构建自定义 widget 需要通过 组合 一些小的 widget(而不是对它们进行扩展)来实现。

In iOS, you typically subclass UIView, or use a pre-existing view, to override and implement methods that achieve the desired behavior. In Flutter, build a custom widget by composing smaller widgets (instead of extending them).

例如,应该如何构建一个初始方法中就包含文本标签的 CustomButton?需要创建一个合成一个 RaisedButton 和一个文本标签的 CustomButton,而不是继承 RaisedButton

For example, how do you build a CustomButton that takes a label in the constructor? Create a CustomButton that composes a ElevatedButton with a label, rather than by extending ElevatedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: () {}, child: Text(label));
  }
}

与其他 Flutter widget 一样的用法,下面我们使用 CustomButton

Then use CustomButton, just as you’d use any other Flutter widget:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Navigation

如何在两个页面之间切换?

How do I navigate between pages?

在 iOS 里,想要在多个 viewcontroller 中切换,可以使用 UINavigationController 管理 viewcontroller 构成的栈进行显示。

In iOS, to travel between view controllers, you can use a UINavigationController that manages the stack of view controllers to display.

Flutter 中也有类似的实现,使用 NavigatorRoutes。一个 Route 是应用中屏幕或者页面的抽象概念,而一个 Navigator 是管多个 Routewidget。也可以理解把 Route 理解为 UIViewController。而 Navigator 的工作方式和 iOS 里的 UINavigationController 类似,当你想要进入或退出一个新页面的时候,它也可以进行 push()pop() 操作。

To navigate between pages, you have a couple options: 想要在不同页面间跳转,你有两个选择:

Flutter has a similar implementation, using a Navigator and Routes. A Route is an abstraction for a “screen” or “page” of an app, and a Navigator is a widget that manages routes. A route roughly maps to a UIViewController. The navigator works in a similar way to the iOS UINavigationController, in that it can push() and pop() routes depending on whether you want to navigate to, or back from, a view.

下面的示例构建了一个 Map

The following example builds a Map.

void main() {
  runApp(CupertinoApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

通过把 route 名称传递给 Navigator 来实现 push 效果。

Navigate to a route by pushing its name to the Navigator.

Navigator.of(context).pushNamed('/b');

Navigator 类对 Flutter 中的路由事件做处理,还可以用来获取入栈之后的路由的结果。这需要通过 push() 返回的 Future 中的 await 来实现。

The Navigator class handles routing in Flutter and is used to get a result back from a route that you have pushed on the stack. This is done by awaiting on the Future returned by push().

例如,要打开一个“定位”页面来让用户选择他们的位置,你需要做如下事情:

For example, to start a ‘location’ route that lets the user select their location, you might do the following:

Map coordinates = await Navigator.of(context).pushNamed('/location');

然后,在”定位“页面中,一旦用户选择了自己的定位,就 pop() 出栈并返回结果。

And then, inside your ‘location’ route, once the user has selected their location, pop() the stack with the result:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

如何跳转到其他应用?

How do I navigate to another app?

在 iOS 里,想要跳转到其他应用,可以使用特定的 URL scheme。对于系统级别的应用,scheme 都是取决于应用的。在 Flutter 里想要实现这个功能,需要创建原生平台的整合层,或者使用已经存在的 插件,例如 url_launcher

In iOS, to send the user to another application, you use a specific URL scheme. For the system level apps, the scheme depends on the app. To implement this functionality in Flutter, create a native platform integration, or use an existing plugin, such as url_launcher.

如何退回到 iOS 原生的 viewcontroller?

How do I pop back to the iOS native viewcontroller?

在 Dart 代码中调用 SystemNavigator.pop() 将会调用下面的 iOS 代码:

Calling SystemNavigator.pop() from your Dart code invokes the following iOS code:

UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  if ([viewController isKindOfClass:[UINavigationController class]]) {
    [((UINavigationController*)viewController) popViewControllerAnimated:NO];
  }

如果这不是你需要的功能,你可以创建你自己的 平台通道 来调用对应的 iOS 代码。

If that doesn’t do what you want, you can create your own platform channel to invoke arbitrary iOS code.

线程和异步

Threading & asynchronicity

如何编写异步代码?

How do I write asynchronous code?

Dart 是单线程执行模型,支持 Isolate(一种在其他线程运行 Dart 代码的方法)、事件循环和异步编程。除非生成了 Isolate,否则所有 Dart 代码将永远在主 UI 线程运行,并由事件循环驱动。Flutter 中的事件循环类似于 iOS 中的 main loop—,也就是主线程上的 Looper

Dart has a single-threaded execution model, with support for Isolates (a way to run Dart code on another thread), an event loop, and asynchronous programming. Unless you spawn an Isolate, your Dart code runs in the main UI thread and is driven by an event loop. Flutter’s event loop is equivalent to the iOS main loop—that is, the Looper that is attached to the main thread.

Dart 的单线程模型并不意味着你需要以阻塞 UI 的形式来执行代码,相反,你更应该使用 Dart 语言提供的异步功能,比如使用 async/await 来实现异步操作。

Dart’s single-threaded model doesn’t mean you are required to run everything as a blocking operation that causes the UI to freeze. Instead, use the asynchronous facilities that the Dart language provides, such as async/await, to perform asynchronous work.

例如,你可以使用 async/await 来执行网络代码以避免 UI 挂起,让 Dart 来完成这个繁重的任务:

For example, you can run network code without causing the UI to hang by using async/await and letting Dart do the heavy lifting:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

一旦 await 等待的网络操作结束,通过调用 setState() 来更新 UI,这将会触发 widget 子树的重新构建并更新数据。

Once the awaited network call is done, update the UI by calling setState(), which triggers a rebuild of the widget sub-tree and updates the data.

下面的示例展示了如何异步加载数据,并在 ListView 中展示出来:

The following example loads data asynchronously and displays it in a ListView:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

更多关于在后台执行任务的信息,以及 Flutter 和 iOS 的区别,可以参考下一章节。

Refer to the next section for more information on doing work in the background, and how Flutter differs from iOS.

如何让你的任务在后台线程执行?

How do you move work to a background thread?

由于 Flutter 是单线程模型,而且执行着一个 event loop(就像 Node.js),你不需要为线程管理或是开启后台线程操心。如果你在处理 I/O 操作,例如磁盘访问或网络请求,那么你安全地使用 async/await 就可以了。但是,如果你需要大量的计算来让 CPU 保持忙碌状态,你需要使用 Isolate 来防治阻塞 event loop。

Since Flutter is single threaded and runs an event loop (like Node.js), you don’t have to worry about thread management or spawning background threads. If you’re doing I/O-bound work, such as disk access or a network call, then you can safely use async/await and you’re done. If, on the other hand, you need to do computationally intensive work that keeps the CPU busy, you want to move it to an Isolate to avoid blocking the event loop.

对于 I/O 操作,把方法声明为 async 方法,然后通过 await 来等待异步方法的执行完成:

For I/O-bound work, declare the function as an async function, and await on long-running tasks inside the function:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

这就是处理网络或数据库请求等 I/O 操作的经典做法。

This is how you typically do network or database calls, which are both I/O operations.

然而,有时候你需要处理大量的数据,从而导致 UI 挂起。在 Flutter 里,当处理长期运行或者运算密集的任务时,可以使用 Isolate 来发挥出多核 CPU 的优势。

However, there are times when you might be processing a large amount of data and your UI hangs. In Flutter, use Isolates to take advantage of multiple CPU cores to do long-running or computationally intensive tasks.

Isolates 是相互隔离的执行线程,并不和主线程共享内存。这意味着你不能够访问主线程的变量,也不能使用 setState() 来更新 UI。Isolates 正如起字面意思是不能共享内存(例如静态变量表)的。

Isolates are separate execution threads that do not share any memory with the main execution memory heap. This means you can’t access variables from the main thread, or update your UI by calling setState(). Isolates are true to their name, and cannot share memory (in the form of static fields, for example).

下面的例子展示了在一个简单的 isolate 中,如何把数据推到主线程上用来更新 UI。

The following example shows, in a simple isolate, how to share data back to the main thread to update the UI.

loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(
    sendPort,
    "https://jsonplaceholder.typicode.com/posts",
  );

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate
static dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

在这里,dataLoader 就是运行在独立线程上的 Isolate。在 Isolate 中,你可以处理 CPU 密集型任务(如解析一个庞大的 JSON 文件),或者处理复杂的数学运算,比如加密操作或者信号处理等。

Here, dataLoader() is the Isolate that runs in its own separate execution thread. In the isolate you can perform more CPU intensive processing (parsing a big JSON, for example), or perform computationally intensive math, such as encryption or signal processing.

下面是一个完整示例:

You can run the full example below:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(
      sendPort,
      "https://jsonplaceholder.typicode.com/posts",
    );

    setState(() {
      widgets = msg;
    });
  }

// the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

如何发起网络请求?

How do I make network requests?

在 Flutter 里,想要构造网络请求十分简单,直接使用 http 即可。它把你可能要实现的网络操作进行了抽象封装,让处理网络请求变得十分简单。

Making a network call in Flutter is easy when you use the popular http package. This abstracts away a lot of the networking that you might normally implement yourself, making it simple to make network calls.

要使用 http 库,需要在 pubspec.yaml 中把它添加为依赖:

To use the http package, add it to your dependencies in pubspec.yaml:

dependencies:
  ...
  http: ^0.11.3+16

构造网络请求,需要在 async 方法 http.get() 中调用 await

To make a network call, call await on the async function http.get():

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

展示耗时任务的进度

How do I show the progress of a long-running task?

在 iOS 里,在后台运行耗时任务时,会使用 UIProgressView

In iOS, you typically use a UIProgressView while executing a long-running task in the background.

在 Flutter 里,应该使用 ProgressIndicator。它在渲染时通过一个 boolean flag 来控制是否显示进度。在耗时任务开始前,告诉 Flutter 去更新状态,并在任务结束后隐藏。

In Flutter, use a ProgressIndicator widget. Show the progress programmatically by controlling when it’s rendered through a boolean flag. Tell Flutter to update its state before your long-running task starts, and hide it after it ends.

在下面的例子中,build 函数被分为三个不同的函数。当 showLoadingDialog()true 时(当 widgets.length == 0),渲染 ProgressIndicator。否则,使用网络请求返回的数据渲染 ListView

In the example below, the build function is separated into three different functions. If showLoadingDialog() is true (when widgets.length == 0), then render the ProgressIndicator. Otherwise, render the ListView with the data returned from a network call.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

工程结构,本地化,依赖和资源

Project structure, localization, dependencies and assets

如何在 Flutter 中引入图片资源?如何处理多分辨率?

How do I include image assets for Flutter? What about multiple resolutions?

在 iOS 里,图片和其他资源会被视为不同的资源分别处理,而在 Flutter 中只有资源这一个概念。 iOS 里被放置在 Images.xcasset 文件夹的资源在 Flutter 中都被放置到了 assets 文件夹中。和 iOS 一样,assets 中可以放置任意类型的文件,而不仅仅是图片。例如,你可以把一个 JSON 文件放置到 my-assets 文件夹中。

While iOS treats images and assets as distinct items, Flutter apps have only assets. Resources that are placed in the Images.xcasset folder on iOS, are placed in an assets folder for Flutter. As with iOS, assets are any type of file, not just images. For example, you might have a JSON file located in the my-assets folder:

my-assets/data.json

pubspec.yaml 中声明 assets:

Declare the asset in the pubspec.yaml file:

assets:
 - my-assets/data.json

然后在代码中通过 AssetBundle 访问资源:

And then access it from code using an AssetBundle:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('my-assets/data.json');
}

对于图片,Flutter 和 iOS 一样遵循了一个简单的基于屏幕密度的格式。 Image assets 可能是 1.0x2.0x3.0x 或者其他任意的倍数。而 devicePixelRatio 则表达了物理分辨率到逻辑分辨率的对照比例。

For images, Flutter follows a simple density-based format like iOS. Image assets might be 1.0x, 2.0x, 3.0x, or any other multiplier. The so-called devicePixelRatio expresses the ratio of physical pixels in a single logical pixel.

Assets 可以放在任何属性的文件夹中—Flutter 没有任何预置的文件结构。你需要在 pubspec.yaml 中声明 assets (包括路径),然后 Flutter 将会识别它们。

Assets are located in any arbitrary folder—Flutter has no predefined folder structure. You declare the assets (with location) in the pubspec.yaml file, and Flutter picks them up.

例如,要添加一个名为 my_icon.png 的图片到你的 Flutter 工程中,你可以把它存储在 images 文件夹下。把基础的图片(一倍图)放到 images 文件夹下,然后把其他倍数的图片放置到对应的比例下的子文件夹中。

For example, to add an image called my_icon.png to your Flutter project, you might decide to store it in a folder arbitrarily called images. Place the base image (1.0x) in the images folder, and the other variants in sub-folders named after the appropriate ratio multiplier:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接着,在 pubspec.yaml 文件中声明这些图片:

Next, declare these images in the pubspec.yaml file:

assets:
 - images/my_icon.png

现在你可以使用 AssetImage 访问你的图片了:

You can now access your images using AssetImage:

return AssetImage("images/a_dot_burr.jpeg");

或者直接在 Image widget 进行使用:

or directly in an Image widget:

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

关于更多的细节,请参见文档 在 Flutter 中添加资源和图片

For more details, see Adding Assets and Images in Flutter.

字符串存储在哪里?如何处理本地化?

Where do I store strings? How do I handle localization?

iOS 里有 Localizable.strings 文件,而 Flutter 则不同,目前并没有关于字符串的处理系统。目前,最佳的方案就是在静态区声明你的文本,然后进行访问。例如:

Unlike iOS, which has the Localizable.strings file, Flutter doesn’t currently have a dedicated system for handling strings. At the moment, the best practice is to declare your copy text in a class as static fields and access them from there. For example:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

你可以这样访问字符串:

You can access your strings as such:

Text(Strings.welcomeMessage)

默认情况下,Flutter 只支持美式英语的本地化字符串。如果你需要添加其他语言支持,请引入 flutter_localizations 库。同时你可能还需要添加 intl 库来使用 i10n 机制,比如日期 / 时间的格式化等。

By default, Flutter only supports US English for its strings. If you need to add support for other languages, include the flutter_localizations package. You might also need to add Dart’s intl package to use i10n machinery, such as date/time formatting.

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

To use the flutter_localizations package, specify the localizationsDelegates and supportedLocales on the app widget:

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: const [
   // Add app-specific localization delegate[s] here
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: const [
    Locale('en', 'US'), // English
    Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

supportedLocales 指定了应用支持的语言,而这些 delegates 则包含了实际的本地化内容。上面的示例使用了一个 MaterialApp,所以它既使用了处理基础 widget 本地化的 GlobalWidgetsLocalizations,也使用了处理 Material widget 本地化的 MaterialWidgetsLocalizations。如果你在应用中使用的是 WidgetApp,就不需要后者了。注意,这两个 delegates 虽然都包含了“默认”值,但是如果你想要实现本地化,就必须在本地提供一个或多个 delegates 的实现副本。

The delegates contain the actual localized values, while the supportedLocales defines which locales the app supports. The above example uses a MaterialApp, so it has both a GlobalWidgetsLocalizations for the base widgets localized values, and a MaterialWidgetsLocalizations for the Material widgets localizations. If you use WidgetsApp for your app, you don’t need the latter. Note that these two delegates contain “default” values, but you’ll need to provide one or more delegates for your own app’s localizable copy, if you want those to be localized too.

当初始化的时候,WidgetsApp(或 MaterialApp)会根据你提供的 delegates 创建一个 Localizations widget。 Localizations widget 可以随时从当前上下文中中获取设备所用的语言,也可以使用 Window.locale

When initialized, the WidgetsApp (or MaterialApp) creates a Localizations widget for you, with the delegates you specify. The current locale for the device is always accessible from the Localizations widget from the current context (in the form of a Locale object), or using the Window.locale.

要使用本地化资源,使用 Localizations.of() 方法可以访问提供代理的特定本地化类。使用 intl_translation 库解压翻译的副本到 arb 文件,然后在应用中通过 intl 来引用它们。

To access localized resources, use the Localizations.of() method to access a specific localizations class that is provided by a given delegate. Use the intl_translation package to extract translatable copy to arb files for translating, and importing them back into the app for using them with intl.

关于 Flutter 中国际化和本地化的细节内容,请参看 Flutter 应用里的国际化,里面包含有使用和不使用 intl 库的示例代码。

For further details on internationalization and localization in Flutter, see the internationalization guide, which has sample code with and without the intl package.

注意在 Flutter 1.0 beta 2 之前,在 Flutter 里定义的资源是不能被原生代码访问的,反之亦然,而原生的资源也是不能在 Flutter 中使用,因为它们都被放在了独立的文件夹中。

Note that before Flutter 1.0 beta 2, assets defined in Flutter were not accessible from the native side, and vice versa, native assets and resources weren’t available to Flutter, as they lived in separate folders.

CocoaPods 相当于 Flutter 中的什么?如何添加依赖?

What is the equivalent of CocoaPods? How do I add dependencies?

在 iOS 里,可以通过 Podfile 添加依赖。而 Flutter 使用 Dart 构建系统和 Pub 包管理器来处理依赖。这些工具将原生应用的打包任务分发给相应 Android 或 iOS 构建系统。

In iOS, you add dependencies by adding to your Podfile. Flutter uses Dart’s build system and the Pub package manager to handle dependencies. The tools delegate the building of the native Android and iOS wrapper apps to the respective build systems.

如果你的 Flutter 项目 iOS 文件夹中存在 Podfile,那么请仅在里面添加原生平台的依赖。总而言之,在 Flutter 中使用 pubspec.yaml 来声明外部依赖。你可以通过 pub.dev 来查找一些优秀的 Flutter 第三方包。

While there is a Podfile in the iOS folder in your Flutter project, only use this if you are adding native dependencies needed for per-platform integration. In general, use pubspec.yaml to declare external dependencies in Flutter. A good place to find great packages for Flutter is on pub.dev.

ViewControllers

ViewControllers 相当于 Flutter 中的什么?

What is the equivalent to ViewController in Flutter?

在 iOS 里,一个 ViewController 是用户界面的一部分,通常是作为屏幕或者其中的一部分来使用。这些组合在一起构成了复杂的用户界面,并以此对应用的 UI 做不断的扩充。在 Flutter 中,这一任务又落到了 Widget 这里。就像在导航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因为“万物皆 widget!”。使用 Naivgator 在不同的 Route 之间切换,而不同的路由则代表了不同的屏幕或页面,或是不同的状态,也可能是渲染相同的数据。

In iOS, a ViewController represents a portion of user interface, most commonly used for a screen or section. These are composed together to build complex user interfaces, and help scale your application’s UI. In Flutter, this job falls to Widgets. As mentioned in the Navigation section, screens in Flutter are represented by Widgets since “everything is a widget!” Use a Navigator to move between different Routes that represent different screens or pages, or maybe different states or renderings of the same data.

如何监听 iOS 中的生命周期?

How do I listen to iOS lifecycle events?

在 iOS 里,可以重写 ViewController 的方法来捕获自身的生命周期,或者在 AppDelegate 中注册生命周期的回调。Flutter 中则没有这两个概念,但是你可以通过在 WidgetsBinding 的 observer 中挂钩子,也可以通过监听 didChangeAppLifecycleState() 事件,来实现相应的功能。

In iOS, you can override methods to the ViewController to capture lifecycle methods for the view itself, or register lifecycle callbacks in the AppDelegate. In Flutter you have neither concept, but you can instead listen to lifecycle events by hooking into the WidgetsBinding observer and listening to the didChangeAppLifecycleState() change event.

可监听的生命周期事件有:

The observable lifecycle events are:

inactive
应用当前处于不活跃状态,不接收用户输入事件。这个事件只在 iOS 上有效,Android 中没有类似的状态。

inactive
The application is in an inactive state and is not receiving user input. This event only works on iOS, as there is no equivalent event on Android.

paused
应用当前处于用户不可见状态,不接收用户输入事件,但仍在后台运行。

paused
The application is not currently visible to the user, is not responding to user input, but is running in the background.

resumed
应用可见,也响应用户输入。

resumed
The application is visible and responding to user input.

suspending
应用被挂起,在 iOS 平台没有这一事件。

suspending
The application is suspended momentarily. The iOS platform has no equivalent event.

关于这些状态含义的更多细节,请参看 AppLifecycleStatus 文档

For more details on the meaning of these states, see AppLifecycleState documentation.

布局

Layouts

UITableViewUICollectionView 相当于 Flutter 中的什么?

What is the equivalent of UITableView or UICollectionView in Flutter?

在 iOS 里,你可能使用 UITableView 或者 UICollectionView 来展示一个列表。而在 Flutter 里,你可以使用 ListView 来达到类似的实现。在 iOS 中,你通过 delegate 方法来确定显示的行数,相应位置的 cell,以及 cell 的尺寸。

In iOS, you might show a list in either a UITableView or a UICollectionView. In Flutter, you have a similar implementation using a ListView. In iOS, these views have delegate methods for deciding the number of rows, the cell for each index path, and the size of the cells.

由于 Flutter 中 widget 的不可变特性,你需要向 ListView 传递一个 widget 列表,Flutter 会确保滚动快速而流畅。

Due to Flutter’s immutable widget pattern, you pass a list of widgets to your ListView, and Flutter takes care of making sure that scrolling is fast and smooth.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

如何确定列表中被点击的元素?

How do I know which list item is clicked?

在 iOS 里,可以通过 tableView:didSelectRowAtIndexPath: 代理方法来实现。而在 Flutter 里,需要通过 widget 传递进来的 touch 响应处理来实现。

In iOS, you implement the delegate method, tableView:didSelectRowAtIndexPath:. In Flutter, use the touch handling provided by the passed-in widgets.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i"),
        ),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

如何动态更新 ListView

How do I dynamically update a ListView?

在 iOS 里,可以更新列表的数据,然后通过调用 reloadData 方法来通知 tableView 或者 collectionView。

In iOS, you update the data for the list view, and notify the table or collection view using the reloadData method.

在 Flutter 里,如果你在 setState() 中更新了 widget 列表,你会发现展示的数据并不会立刻更新。这是因为当 setState() 被调用时,Flutter 的渲染引擎回去检索 widget 树是否有改变。当它获取到 ListView,会进行 == 判断,然后发现两个 ListView 是相等的。没发现有改变,所以也就不会进行更新。

In Flutter, if you update the list of widgets inside a setState(), you quickly see that your data doesn’t change visually. This is because when setState() is called, the Flutter rendering engine looks at the widget tree to see if anything has changed. When it gets to your ListView, it performs an == check, and determines that the two ListViews are the same. Nothing has changed, so no update is required.

一个更新 ListView 的简单方法就是,在 setState() 创建一个新的 List,然后拷贝旧列表中的所有数据到新列表。这样虽然简单,但是像下面示例一样数据量很大时,并不推荐这样做。

For a simple way to update your ListView, create a new List inside of setState(), and copy the data from the old list to the new list. While this approach is simple, it is not recommended for large data sets, as shown in the next example.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
    );
  }
}

一个推荐的、高效且有效的方法就是使用 ListView.Builder 来构建列表。当你的数据量很大,且需要构建动态列表时,这个方法会非常好用。

The recommended, efficient, and effective way to build a list uses a ListView.Builder. This method is great when you have a dynamic list or a list with very large amounts of data.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
    );
  }
}

和创建 ListView 不同,创建 ListView.Builder 需要两个关键参数:初始化列表长度和 ItemBuilder 函数。

Instead of creating a ListView, create a ListView.builder that takes two key parameters: the initial length of the list, and an ItemBuilder function.

ItemBuilder 函数和 iOS 里 tableView 和 collectionView 的 cellForItemAt 方法类似,它接收位置参数,然后返回想要在该位置渲染的 cell。

The ItemBuilder function is similar to the cellForItemAt delegate method in an iOS table or collection view, as it takes a position, and returns the cell you want rendered at that position.

最后,也是最重要的,注意 onTap() 方法并没有重新创建列表,而是使用 .add 方法进行添加。

Finally, but most importantly, notice that the onTap() function doesn’t recreate the list anymore, but instead .adds to it.

ScrollView 相当于 Flutter 中的什么?

What is the equivalent of a ScrollView in Flutter?

在 iOS 里,把视图放在 ScrollView 里来允许用户在需要时滚动内容。

In iOS, you wrap your views in a ScrollView that allows a user to scroll your content if needed.

在 Flutter 中,最简单的办法就是使用 ListView widget。它和 iOS 中的 ScrollView 以及 TableView 表现一致,也可以给它的子 widget 做垂直排版。

In Flutter the easiest way to do this is using the ListView widget. This acts as both a ScrollView and an iOS TableView, as you can layout widgets in a vertical format.

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

关于 Flutter 中布局的更多细节,请参看 布局教程

For more detailed docs on how to lay out widgets in Flutter, see the layout tutorial.

手势检测与 touch 事件处理

Gesture detection and touch event handling

如何给 Flutter 的 widget 添加点击事件?

How do I add a click listener to a widget in Flutter?

在 iOS 里,通过把 GestureRecognizer 绑定给 UIView 来处理点击事件。在 Flutter 中,有两种方法来添加事件监听者:

In iOS, you attach a GestureRecognizer to a view to handle click events. In Flutter, there are two ways of adding touch listeners:

  1. 如果 widget 本身支持事件检测,则直接传递处理函数给它。例如,ElevatedButton 拥有一个 onPressed 参数:

    If the widget supports event detection, pass a function to it, and handle the event in the function. For example, the ElevatedButton widget has an onPressed parameter:

    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          print("click");
        },
        child: Text("Button"),
      );
    }
    
  2. 如果 widget 本身不支持事件检测,那么把它封装到一个 GestureDetector 中,并给它的 onTap 参数传递一个函数:

    If the Widget doesn’t support event detection, wrap the widget in a GestureDetector and pass a function to the onTap parameter.

    class SampleApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: GestureDetector(
              child: FlutterLogo(
                size: 200.0,
              ),
              onTap: () {
                print("tap");
              },
            ),
          ),
        );
      }
    }
    

如何处理 widget 的其他手势?

How do I handle other gestures on widgets?

你可以使用 GestureDetector 来监听更多的手势,例如:

Using GestureDetector you can listen to a wide range of gestures such as:

下面的示例展示了 GestureDetector 是如何实现双击时旋转 Flutter 的 logo 的:

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  super.initState();
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: RotationTransition(
            turns: curve,
            child: FlutterLogo(
              size: 200.0,
            )),
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
        ),
      ),
    );
  }
}

主题和文字

Theming and text

如何设置应用主题?

How do I theme an app?

Flutter 实现了一套漂亮的 Material Design 组件,而且开箱可用,它提供了许多常用的样式和主题。

Out of the box, Flutter comes with a beautiful implementation of Material Design, which takes care of a lot of styling and theming needs that you would typically do.

为了充分发挥应用中 Material Components 的优势,声明一个顶级的 widget,MaterialApp,来作为你的应用入口。 MaterialApp 是一个封装了大量常用 Material Design 组件的 widget。它基于 WidgetsApp 添加了 Material 的相关功能。

To take full advantage of Material Components in your app, declare a top-level widget, MaterialApp, as the entry point to your application. MaterialApp is a convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design. It builds upon a WidgetsApp by adding Material specific functionality.

但是 Flutter 有足够的灵活性和表现力来实现任何设计语言。在 iOS 上,可以使用 Cupertino library 来制作遵循 人机接口指南 的界面。关于这些 widget 的全部集合,可以参看 Cupertino widgets gallery。

But Flutter is flexible and expressive enough to implement any design language. On iOS, you can use the Cupertino library to produce an interface that adheres to the Human Interface Guidelines. For the full set of these widgets, see the Cupertino widgets gallery.

也可以使用 WidgetApp 来做为应用入口,它提供了一部分类似的功能接口,但是不如 MaterialApp 强大。

You can also use a WidgetsApp as your app widget, which provides some of the same functionality, but is not as rich as MaterialApp.

定义所有子组件颜色和样式,可以直接传递 ThemeData 对象给 MaterialApp widget。例如在下面的代码中,primary swatch 被设置为蓝色,而文本选中后的颜色被设置为红色。

To customize the colors and styles of any child components, pass a ThemeData object to the MaterialApp widget. For example, in the code below, the primary swatch is set to blue and text selection color is red.

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}

如何给 Text widget 设置自定义字体?

How do I set custom fonts on my Text widgets?

在 iOS 里,可以在项目中引入任何的 ttf 字体文件,并在 info.plist 文件中声明并进行引用。在 Flutter 里,把字体放到一个文件夹中,然后在 pubspec.yaml 文件中引用它,就和引用图片一样。

In iOS, you import any ttf font files into your project and create a reference in the info.plist file. In Flutter, place the font file in a folder and reference it in the pubspec.yaml file, similar to how you import images.

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

然后在 Text widget 中指定字体:

Then assign the font to your Text widget:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何设置 Text widget 的样式?

How do I style my Text widgets?

除了字体以外,你也可以自定义 Text widget 的其他样式。Text widget 接收一个 TextStyle 对象的参数,可以指定很多参数,例如:

Along with fonts, you can customize other styling elements on a Text widget. The style parameter of a Text widget takes a TextStyle object, where you can customize many parameters, such as:

表单输入

Form input

Flutter 中如何使用表单?如何获取到用户的输入?

How do forms work in Flutter? How do I retrieve user input?

我们知道 Flutter 使用的是不可变而且状态分离的 widget,你可能会好奇这种情况下如何处理用户的输入。在 iOS 上,一般会在提交数据时查询当前组件的数值或动作。那么在 Flutter 中会怎么样呢?

Given how Flutter uses immutable widgets with a separate state, you might be wondering how user input fits into the picture. On iOS, you usually query the widgets for their current values when it’s time to submit the user input, or action on it. How does that work in Flutter?

和 Flutter 的其他部分一样,表单处理要通过特定的 widget 来实现。如果你有一个 TextField 或者 TextFormField,你可以通过 TextEditingController 来获取用户的输入:

In practice forms are handled, like everything in Flutter, by specialized widgets. If you have a TextField or a TextFormField, you can supply a TextEditingController to retrieve user input:

class _MyFormState extends State<MyForm> {
  // Create a text controller and use it to retrieve the current value.
  // of the TextField!
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the Widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: myController,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text the user has typed into our text field.
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text the user has typed in using our
                // TextEditingController
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: Icon(Icons.text_fields),
      ),
    );
  }
}

你在 Flutter Cookbook获取文本框的输入值 教程中可以找到更多的相关内容以及详细的代码列表。

You can find more information and the full code listing in Retrieve the value of a text field, from the Flutter cookbook.

TextField 中的 placeholder 相当于什么?

What is the equivalent of a placeholder in a text field?

在 Flutter 里,通过向 Text widget 传递一个 InputDecoration 对象,你可以轻易的显示文本框的提示信息,或是 placeholder。

In Flutter you can easily show a “hint” or a placeholder text for your field by adding an InputDecoration object to the decoration constructor parameter for the Text widget:

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  ),
)

如何展示验证错误信息?

How do I show validation errors?

就和显示提示信息一样,你可以通过向 Text widget 传递一个 InputDecoration 来实现。

Just as you would with a “hint”, pass an InputDecoration object to the decoration constructor for the Text widget.

然而,你并不想在一开始就显示错误信息。相反,在用户输入非法数据后,应该更新状态,并传递一个新的 InputDecoration 对象。

However, you don’t want to start off by showing an error. Instead, when the user has entered invalid data, update the state, and pass a new InputDecoration object.

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String emailString) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(emailString);
  }
}

和硬件、第三方服务以及系统平台交互

Interacting with hardware, third party services and the platform

如何与系统平台以及平台原生代码进行交互?

How do I interact with the platform, and with platform native code?

Flutter 并不直接在平台上运行代码;而是以 Dart 代码的方式原生运行于设备之上,这算是绕过了平台的 SDK 的限制。这意味着,例如,你用 Dart 发起了一个网络请求,它会直接在 Dart 的上下文中运行。你不需要调用写 iOS 或者 Android 原生应用时常用的 API 接口。你的 Flutter 应用仍旧被原生平台的 ViewController 当做一个 view 来管理,但是你不能够直接访问 ViewController 自身或是对应的原生框架。

Flutter doesn’t run code directly on the underlying platform; rather, the Dart code that makes up a Flutter app is run natively on the device, “sidestepping” the SDK provided by the platform. That means, for example, when you perform a network request in Dart, it runs directly in the Dart context. You don’t use the Android or iOS APIs you normally take advantage of when writing native apps. Your Flutter app is still hosted in a native app’s ViewController as a view, but you don’t have direct access to the ViewController itself, or the native framework.

这并不意味着 Flutter 应用不能够和原生 API,或是原生代码进行交互。 Flutter 提供了用来和宿主 ViewController 通信和交换数据的 平台通道。平台通道本质上是一个桥接了 Dart 代码与宿主 ViewController 和 iOS 框架的异步通信模型。你可以通过平台通道来执行原生代码的方法,或者获取设备的传感器信息等数据。

This doesn’t mean Flutter apps cannot interact with those native APIs, or with any native code you have. Flutter provides the platform channel, which communicates and exchanges data with the ViewController that hosts your Flutter view. Platform channels are essentially an asynchronous messaging mechanism that bridge the Dart code with the host ViewController and the iOS framework it runs on. You can use platform channels to execute a method on the native side, or to retrieve some data from the device’s sensors, for example.

除了直接使用 platform channels 之外,也可以使用一系列包含了原生代码和 Dart代码,实现了特定功能的现有 插件。例如,你在 Flutter 中可以直接使用插件来访问相册或是设备摄像头,而不需要自己重新集成。 pub.dev 是一个 Dart 和 Flutter 的开源包仓库,你可以在这里找到需要的插件。有些包可能支持集成 iOS 或 Android,或两者皆有。

In addition to directly using platform channels, you can use a variety of pre-made plugins that encapsulate the native and Dart code for a specific goal. For example, you can use a plugin to access the camera roll and the device camera directly from Flutter, without having to write your own integration. Plugins are found on pub.dev, Dart and Flutter’s open source package repository. Some packages might support native integrations on iOS, Android, web, or all of the above.

如果你在 Pub 找不到自己需要的包,你可以自己写一个,相关信息可以查阅 Flutter Packages 的开发和提交,并且你可以将其 发布到 Pub 上

If you can’t find a plugin on pub.dev that fits your needs, you can write your own and publish it on pub.dev.

如何访问 GPS 传感器?

How do I access the GPS sensor?

使用 geolocator 插件,这一插件由社区提供。

Use the geolocator community plugin.

如何访问相机?

How do I access the camera?

image_picker 是常用的访问相机的插件。

The image_picker plugin is popular for accessing the camera.

如何使用 Facebook 登录?

How do I log in with Facebook?

登录 Facebook 可以使用 flutter_facebook_login 插件。

To log in with Facebook, use the flutter_facebook_login community plugin.

如何集成 Firebase 的功能?

How do I use Firebase features?

大多数的 Firebase 特性都在 官方维护的插件 中实现了。这些插件由 Flutter 官方团队维护:

Most Firebase functions are covered by first party plugins. These plugins are first-party integrations, maintained by the Flutter team:

在 pub.dev 上你也可以找到一些第三方的 Firebase 插件,主要实现了官方插件没有直接实现的功能。

You can also find some third-party Firebase plugins on pub.dev that cover areas not directly covered by the first-party plugins.

如何构建自己的插件?

How do I build my own custom native integrations?

如果有一些 Flutter 和遗漏的平台特性,可以根据 developing packages and plugins 构建自己的插件。

If there is platform-specific functionality that Flutter or its community Plugins are missing, you can build your own following the developing packages and plugins page.

Flutter 的插件结构,简单来说,更像是 Android 中的 Event bus:你发送一个消息,并让接受者处理并反馈结果给你。这种情况下,接受者就是在 iOS 或 Android 的原生代码。

Flutter’s plugin architecture, in a nutshell, is much like using an Event bus in Android: you fire off a message and let the receiver process and emit a result back to you. In this case, the receiver is code running on the native side on Android or iOS.

数据库和本地存储

Databases and local storage

Flutter 中如何访问 UserDefaults?

How do I access UserDefault in Flutter?

在 iOS 里,可以使用属性列表存储一个键值对的集合,也就是我们所说的 UserDefaults

In iOS, you can store a collection of key-value pairs using a property list, known as the UserDefaults.

在 Flutter 里,可以使用 Shared Preferences 插件 来实现相同的功能。这个插件封装了 UserDefaults 以及 Android 里类似的 SharedPreferences

In Flutter, access equivalent functionality using the Shared Preferences plugin. This plugin wraps the functionality of both UserDefaults and the Android equivalent, SharedPreferences.

CoreData 相当于 Flutter 中的什么?

What is the equivalent to CoreData in Flutter?

在 iOS 里,你可以使用 CoreData 来存储结构化的数据。这是一个基于 SQL 数据库的上层封装,可以使关联模型的查询变得更加简单。

In iOS, you can use CoreData to store structured data. This is simply a layer on top of an SQL database, making it easier to make queries that relate to your models.

在 Flutter 里,可以使用 SQFlite 插件来实现这个功能。

In Flutter, access this functionality using the SQFlite plugin.

Debugging

应该使用什么工具调试我的 Flutter 应用?

What tools can I use to debug my app in Flutter?

请使用 开发者工具 debug 你的 Flutter 和 Dart 应用。

Use the DevTools suite for debugging Flutter or Dart apps.

开发者工具包含了 profiling 构建、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 开发者工具 文档。

DevTools includes support for profiling, examining the heap, inspecting the widget tree, logging diagnostics, debugging, observing executed lines of code, debugging memory leaks and memory fragmentation. For more information, see the DevTools documentation.

通知

Notifications

如何设置推送通知?

How do I set up push notifications?

在 iOS 里,你需要向开发者中心注册来允许推送通知。

In iOS, you need to register your app on the developer portal to allow push notifications.

在 Flutter 里,使用 firebase_messaging 插件来实现这个功能。

In Flutter, access this functionality using the firebase_messaging plugin.

关于 Firebase Cloud Messaging API 的更多信息,可以查看 firebase_messaging 插件文档。

For more information on using the Firebase Cloud Messaging API, see the firebase_messaging plugin documentation.

给 React Native 开发者的 Flutter 指南

目录

本文面向希望基于现有的 React Native 的知识结构使用 Flutter 开发移动端应用的开发者。如果你已经对 RN 的框架有所了解,那么你可以通过这个文档入门 Flutter 开发。

This document is for React Native (RN) developers looking to apply their existing RN knowledge to build mobile apps with Flutter. If you understand the fundamentals of the RN framework then you can use this document as a way to get started learning Flutter development.

本文可以当做查询手册使用,里面涉及到的问题基本上可以满足需求。

This document can be used as a cookbook by jumping around and finding questions that are most relevant to your needs.

针对 JavaScript 开发者的 Dart 介绍

Introduction to Dart for JavaScript Developers

和 React Native 一样,Flutter 使用 reactive 风格的视图。然而,RN 需要被转译为本地对应的 widget,而 Flutter 是直接编译成本地原生代码。 Flutter 可以控制屏幕上的每一个像素,如此可以避免由于使用 JavaScript Bridge 导致的性能问题。

Like React Native, Flutter uses reactive-style views. However, while RN transpiles to native widgets, Flutter compiles all the way to native code. Flutter controls each pixel on the screen, which avoids performance problems caused by the need for a JavaScript bridge.

Dart 学习起来非常简单而且有如下特性:

Dart is an easy language to learn and offers the following features:

下面的几个例子解释了 JavaScript 和 Dart 的区别。

A few examples of the differences between JavaScript and Dart are described below.

入口函数

Entry point

JavaScript 并没有预定义的入口函数。

JavaScript doesn’t have a pre-defined entry function—you define the entry point.

// JavaScript
function startHere() {
  // Can be used as entry point
}

在 Dart 里,每个应用程序必须有一个最顶级的 main() 函数,该函数作为应用程序的入口函数。

In Dart, every app must have a top-level main() function that serves as the entry point to the app.

// Dart
main() {
}

可以在这里查看效果 DartPad

Try it out in DartPad.

在控制台打印输出

Printing to the console

在 Dart 中如果需要在控制台进行输出,调用 print()

To print to the console in Dart, use print().

// JavaScript
console.log('Hello world!');
// Dart
print('Hello world!');

可以在这里查看效果 DartPad

Try it out in DartPad.

变量

Variables

Dart 是类型安全的,它结合静态类型检查和运行时检查来保证变量的值总是和变量的静态类型相匹配。虽然类型是语法要求,有些类型标注也并不是必须要填的,因为 Dart 使用类型推断。

Dart is type safe—it uses a combination of static type checking and runtime checks to ensure that a variable’s value always matches the variable’s static type. Although types are mandatory, some type annotations are optional because Dart performs type inference.

创建变量并赋值

Creating and assigning variables

在 JavaScript 中,变量是无法指定类型的。

In JavaScript, variables cannot be typed.

Dart 中,变量要么被显式定义类型,要么系统会自动判断变量的类型。

In Dart, variables must either be explicitly typed or the type system must infer the proper type automatically.

// JavaScript
var name = 'JavaScript';
// Dart
String name = 'dart'; // Explicitly typed as a string.
var otherName = 'Dart'; // Inferred string.
// Both are acceptable in Dart.

可以在这里查看效果 DartPad

Try it out in DartPad.

如果想了解更多相关信息,请参考 Dart 的类型系统

For more information, see Dart’s Type System.

默认值

Default value

在 JavaScript 中,未初始化的变量是 undefined

In JavaScript, uninitialized variables are undefined.

在 Dart 中,未初始化的变量会有一个初始值 null。因为数字在 Dart 是对象,甚至未初始化的数字类型的变量也会是 null

In Dart, uninitialized variables have an initial value of null. Because numbers are objects in Dart, even uninitialized variables with numeric types have the value null.

// JavaScript
var name; // == undefined
// Dart
var name; // == null
int x; // == null

可以在这里查看效果 DartPad

Try it out in DartPad.

如果想了解更多详细内容,请查看这个文档 variables

For more information, see the documentation on variables.

检查 null 或者零值

Checking for null or zero

在 JavaScript 中,1 或者任何非空对象都相当于 true。

In JavaScript, values of 1 or any non-null objects are treated as true.

// JavaScript
var myNull = null;
if (!myNull) {
  console.log('null is treated as false');
}
var zero = 0;
if (!zero) {
  console.log('0 is treated as false');
}

在 Dart 中,只有布尔类型值 true 才是 true。

In Dart, only the boolean value true is treated as true.

// Dart
var myNull = null;
if (myNull == null) {
  print('use "== null" to check null');
}
var zero = 0;
if (zero == 0) {
  print('use "== 0" to check zero');
}

可以在这里查看效果 DartPad

Try it out in DartPad.

函数

Functions

Dart 和 JavaScript 中的函数很相似。最大的区别是声明格式。

Dart and JavaScript functions are generally similar. The primary difference is the declaration.

// JavaScript
function fn() {
  return true;
}
// Dart
fn() {
  return true;
}
// can also be written as
bool fn() {
  return true;
}

可以在这里查看效果 DartPad

Try it out in DartPad.

如果想了解更多相关信息,请转向该页面 functions

For more information, see the documentation on functions.

异步编程

Asynchronous programming

Futures

和 JavaScript 类似,Dart 支持单线程。在 JavaScript 中, Promise 对象代表异步操作的完成或者失败。

Like JavaScript, Dart supports single-threaded execution. In JavaScript, the Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Dart 使用 Future 对象来实现该机制。

Dart uses Future objects to handle this.

// JavaScript
class Example {
  _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    return fetch(url)
      .then(response => response.json())
      .then(responseJson => {
        const ip = responseJson.origin;
        return ip;
      });
  }
}

function main() {
  const example = new Example();
  example
    ._getIPAddress()
    .then(ip => console.log(ip))
    .catch(error => console.error(error));
}

main();
// Dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class Example {
  Future<String> _getIPAddress() {
    final url = new Uri.https('httpbin.org', '/ip');
    return http.get(url).then((response) {
      String ip = jsonDecode(response.body)['origin'];
      return ip;
    });
  }
}

main() {
  final example = new Example();
  example
      ._getIPAddress()
      .then((ip) => print(ip))
      .catchError((error) => print(error));
}

如果想了解更多相关信息,请参考 Future 的相关文档。

For more information, see the documentation on Future objects.

asyncawait

async and await

async 函数声明定义了一个异步函数。

The async function declaration defines an asynchronous function.

在 JavaScript 中, async 函数返回一个 Promiseawait 操作符用于等待 Promise

In JavaScript, the async function returns a Promise. The await operator is used to wait for a Promise.

// JavaScript
class Example {
  async function _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    const response = await fetch(url);
    const json = await response.json();
    const data = json.origin;
    return data;
  }
}

async function main() {
  const example = new Example();
  try {
    const ip = await example._getIPAddress();
    console.log(ip);
  } catch (error) {
    console.error(error);
  }
}

main();

在 Dart 中,async 函数返回一个 Future,而函数体会在未来执行, await 操作符用于等待 Future

In Dart, an async function returns a Future, and the body of the function is scheduled for execution later. The await operator is used to wait for a Future.

// Dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class Example {
  Future<String> _getIPAddress() async {
    final url = new Uri.https('httpbin.org', '/ip');
    final response = await http.get(url);
    String ip = jsonDecode(response.body)['origin'];
    return ip;
  }
}

main() async {
  final example = new Example();
  try {
    final ip = await example._getIPAddress();
    print(ip);
  } catch (error) {
    print(error);
  }
}

如果想了解更多相关信息,请参考 asyncawait 的相关文档

For more information, see the documentation for async and await.

基本知识

The basics

如何创建一个 Flutter 应用?

How do I create a Flutter app?

如果要使用 React Native 创建应用,你需要在命令行里运行 create-react-native-app

To create an app using React Native, you would run create-react-native-app from the command line.

$ create-react-native-app <projectname>

要在 Flutter 中创建应用,完成下面其中一项即可:

To create an app in Flutter, do one of the following:

$ flutter create <projectname>

如果想要了解更多内容,详见 开始使用 Flutter,在该页面会手把手教你创建一个点击按钮进行计数的应用。创建一个 Flutter 项目就可以构建 Android 和 iOS 设备上运行应用所需的所有文件。

For more information, see Getting started, which walks you through creating a button-click counter app. Creating a Flutter project builds all the files that you need to run a sample app on both Android and iOS devices.

我如何运行应用呢?

How do I run my app?

在 React Native, 你可以在项目文件夹中运行 npm run 或者 yarn run

In React Native, you would run npm run or yarn run from the project directory.

你可以通过如下几个途径运行 Flutter 应用程序:

You can run Flutter apps in a couple of ways:

你的应用程序会在已连接的设备、iOS 模拟器或者 Android 模拟器上运行。

Your app runs on a connected device, the iOS simulator, or the Android emulator.

如果想了解更多相关信息,可以参考 Flutter 的相关文档: 开始使用 Flutter

For more information, see the Flutter Getting Started documentation.

如何导入 widget

How do I import widgets?

在 React Native 中,你需要导入每一个所需的组件。

In React Native, you need to import each required component.

//React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

在 Flutter 中,如果要使用 Material Design 库里的 widget,导入 material.dart 包。如果要使用 iOS 风格的 widget,导入 Cupertino 库。如果要使用更加基本的 widget,导入 Widgets 库。或者,你可以实现自己的 widget 库并导入。

In Flutter, to use widgets from the Material Design library, import the material.dart package. To use iOS style widgets, import the Cupertino library. To use a more basic widget set, import the Widgets library. Or, you can write your own widget library and import that.

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/my_widgets.dart';

无论你导入哪个库,Dart 仅仅引用你应用中用到的 widget。

Whichever widget package you import, Dart pulls in only the widgets that are used in your app.

如果想了解更多相关信息,可以参考 核心 Widget 目录

For more information, see the Flutter Widget Catalog.

在 Flutter 里有没有类似 React Native 中 “Hello world!” 应用程序?

What is the equivalent of the React Native “Hello world!” app in Flutter?

在 React Native,HelloWorldApp 继承自 React.Component 并且通过返回 view 对象实现了 render 方法。

In React Native, the HelloWorldApp class extends React.Component and implements the render method by returning a view component.

// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello world!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

在 Flutter 中,你可以使用核心 widget 库中的 CenterText widget 创建对应的 “Hello world!” 应用程序。 Center widget 是 widget 树中的根,而且只有 Text 一个子 widget。

In Flutter, you can create an identical “Hello world!” app using the Center and Text widgets from the core widget library. The Center widget becomes the root of the widget tree and has one child, the Text widget.

// Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

下面的图片展示了 Android 和 iOS 中的基本 Flutter “Hello world!” 应用程序的界面。

The following images show the Android and iOS UI for the basic Flutter “Hello world!” app.

Hello world app on Android
Android
Hello world app on iOS
iOS

现在大家已经明白了最基本的 Flutter 应用,接下来会告诉大家如何利用 Flutter 丰富的 widget 库来创建主流的华丽的应用程序。

Now that you’ve seen the most basic Flutter app, the next section shows how to take advantage of Flutter’s rich widget libraries to create a modern, polished app.

我如何使用 widget 并且把它们封装起来组成一个 widget 树?

How do I use widgets and nest them to form a widget tree?

在 Flutter 中,几乎任何元素都是 widget。

In Flutter, almost everything is a widget.

widget 是构建应用软件用户界面的基本元素。你可以将 widget 按照一定的层次组合,成为 widget 树。每个 widget 内嵌在父 widget 中,并且继承了父 widget 的属性。甚至应用程序本身就是一个 widget。并没有一个独立的应用程序对象。反而 root widget 充当了这个角色。

Widgets are the basic building blocks of an app’s user interface. You compose widgets into a hierarchy, called a widget tree. Each widget nests inside a parent widget and inherits properties from its parent. Even the application object itself is a widget. There is no separate “application” object. Instead, the root widget serves this role.

一个 widget 可以定义:

A widget can define:

下面的示例展示了使用 Material 库里 widget 实现的 “Hello world!” 应用程序。在这个示例中,该 widget 树是包含在 MaterialApp root widget 里的。

The following example shows the “Hello world!” app using widgets from the Material library. In this example, the widget tree is nested inside the MaterialApp root widget.

// Flutter
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
 widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello world'),
        ),
      ),
    );
  }
}

下面的图片为大家展示了通过 Material Design widget 所实现的 “Hello world!” 应用。你可以获得比 “Hello world!” 应用更多的功能。

The following images show “Hello world!” built from Material Design widgets. You get more functionality for free than in the basic “Hello world!” app.

Hello world app on Android
Android
Hello world app on iOS
iOS

当编写应用代码的时候,你将用到下述两种 widget: 无状态 widget 就像它的名字一样,是一个没有状态的 widget。无状态 widget 一旦创建,就不会改变。而 有状态 widget 会基于接收到的数据或者用户输入的数据动态改变状态。

When writing an app, you’ll use two types of widgets: StatelessWidget or StatefulWidget. A StatelessWidget is just what it sounds like—a widget with no state. A StatelessWidget is created once, and never changes its appearance. A StatefulWidget dynamically changes state based on data received, or user input.

无状态 widget 和有状态 widget 之间的主要区别,是有状态 widget 包含一个 State 对象会缓存状态数据,并且 widget 树的重建也会携带该数据,因此状态不会丢失。

The important difference between stateless and stateful widgets is that StatefulWidgets have a State object that stores state data and carries it over across tree rebuilds, so it’s not lost.

在简单的或者基本的应用程序中,封装 widget 非常简单,但是随着代码量的增加并且应用程序的功能变得更加复杂,你应该将层级复杂的 widget 封装到函数中或者稍小一些的类。创建独立的函数和 widget 可以让你更好地复用应用中组件。

In simple or basic apps it’s easy to nest widgets, but as the code base gets larger and the app becomes complex, you should break deeply nested widgets into functions that return the widget or smaller classes. Creating separate functions and widgets allows you to reuse the components within the app.

如何创建可复用的组件?

How do I create reusable components?

在 React Native 中,你可以定义一个类来创建一个可复用的组件然后使用 props 方法来设置或者返回属性或者所选元素的值。在下面的示例中,CustomCard 类在父类中被定义和调用。

In React Native, you would define a class to create a reusable component and then use props methods to set or return properties and values of the selected elements. In the example below, the CustomCard class is defined and then used inside a parent class.

// React Native
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title="Press"
          onPress={() => this.props.onPress(this.props.index)}
        />
      </View>
    );
  }
}

// Usage
<CustomCard onPress={this.onPress} index={item.key} />

在 Flutter 中,定义一个类来创建一个自定义 widget 然后复用这个 widget。你可以定义并且调用函数来返回一个可复用的 widget,正如下面示例中 build 函数所示的那样。

In Flutter, define a class to create a custom widget and then reuse the widget. You can also define and call a function that returns a reusable widget as shown in the build function in the following example.

// Flutter
class CustomCard extends StatelessWidget {
  CustomCard({@required this.index, @required
     this.onPress});

  final index;
  final Function onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: <Widget>[
          Text('Card $index'),
          TextButton(
            child: const Text('Press'),
            onPressed: this.onPress,
          ),
        ],
      )
    );
  }
}
    ...
// Usage
CustomCard(
  index: index,
  onPress: () {
    print('Card $index');
  },
)
    ...

在之前的示例,CustomCard 类的构造函数使用 Dart 的曲括号 { } 来表示 可选参数

In the previous example, the constructor for the CustomCard class uses Dart’s curly brace syntax { } to indicate named optional parameters.

如果将这些参数设定为必填参数,要么从构造函数中删掉曲括号,或者在构造函数中加上 @required

To require these fields, either remove the curly braces from the constructor, or add @required to the constructor.

下面的截图展示了可复用的 CustomCard 类的示例:

The following screenshots show an example of the reusable CustomCard class.

Custom cards on Android
Android
Custom cards on iOS
iOS

项目结构和资源

Project structure and resources

该从哪开始写代码呢?

Where do I start writing the code?

main.dart 文件开始。这个文件会在你创建 Flutter 应用时自动生成。

Start with the lib/main.dart file. It’s autogenerated when you create a Flutter app.

// Dart
void main(){
 print('Hello, this is the main function.');
}

在 Flutter 中,入口文件是 ’projectname’/lib/main.dart 而程序执行是从 main 函数开始的。

In Flutter, the entry point file is ’projectname’/lib/main.dart and execution starts from the main function.

Flutter 应用程序中的文件是如何组织的?

How are files structured in a Flutter app?

当你创建一个新的 Flutter 工程的时候,它会创建如下所示的文件夹结构。你可以自定义这个结构,不过这是整个开发的起点。

When you create a new Flutter project, it builds the following directory structure. You can customize it later, but this is where you start.

┬
└ projectname
  ┬
  ├ android      - Contains Android-specific files.
  ├ build        - Stores iOS and Android build files.
  ├ ios          - Contains iOS-specific files.
  ├ lib          - Contains externally accessible Dart source files.
    ┬
    └ src        - Contains additional source files.
    └ main.dart  - The Flutter entry point and the start of a new app.
                   This is generated automatically when you create a Flutter
                    project.
                   It's where you start writing your Dart code.
  ├ test         - Contains automated test files.
  └ pubspec.yaml - Contains the metadata for the Flutter app.
                   This is equivalent to the package.json file in React Native.
┬
└ projectname
  ┬
  ├ android      - 包含 Android 相关文件。
  ├ build        - 存储 iOS 和 Android 构建文件。
  ├ ios          - 包含 iOS 相关文件。
  ├ lib          - 包含外部可访问 Dart 源文件。
    ┬
    └ src        - 包含附加源文件。
    └ main.dart  - Flutter 程序入口和新应用程序的起点。当你创建 Flutter 工程的时候会自动生成这些文件。你从这里开始写 Dart 代码
  ├ test         - 包含自动测试文件。
  └ pubspec.yaml - 包含 Flutter 应用程序的元数据。这个文件相当于 React Native 里的 package.json 文件。

我该把资源文件放到哪并且如何调用呢?

Where do I put my resources and assets and how do I use them?

一个 Flutter 资源就是打包到你应用程序里的一个文件并且在程序运行的时候可以访问。 Flutter 应用程序可以包含下述几种资源类型:

A Flutter resource or asset is a file that is bundled and deployed with your app and is accessible at runtime. Flutter apps can include the following asset types:

Flutter 使用 pubspec.yaml 文件来确定应用程序中的资源。该文件在工程的根目录。

Flutter uses the pubspec.yaml file, located at the root of your project, to identify assets required by an app.

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

assets 确定了需要包含在应用程序中的文件。每个资源都会在 pubspec.yaml 中定义所存储的相对路径。资源定义的顺序没有特殊要求。实际的文件夹(在这里指 assets )也没影响。但是,由于资源可以放置于程序的任何目录,所以放在 assets 文件夹是比较好的。

The assets subsection specifies files that should be included with the app. Each asset is identified by an explicit path relative to the pubspec.yaml file, where the asset file is located. The order in which the assets are declared does not matter. The actual directory used (assets in this case) does not matter. However, while assets can be placed in any app directory, it’s a best practice to place them in the assets directory.

在构建期间,Flutter 会将资源放到一个称为 asset bundle 的归档文件中,应用程序可以在运行时访问该文件。当一个资源在 pubspec.yaml 中被声明时,构建进程会查询和这个文件相关的子文件夹路径,这些文件也会被包含在 asset bundle 中。当你为应用程序选择和屏幕显示分辨率相关的图片时, Flutter 会使用 asset variants。

During a build, Flutter places assets into a special archive called the asset bundle, which apps read from at runtime. When an asset’s path is specified in the assets section of pubspec.yaml, the build process looks for any files with the same name in adjacent subdirectories. These files are also included in the asset bundle along with the specified asset. Flutter uses asset variants when choosing resolution-appropriate images for your app.

在 React Native,你可以在源码文件夹中通过添加文件来增加一个静态图片并且在代码中引用它。

In React Native, you would add a static image by placing the image file in a source code directory and referencing it.

<Image source={require('./my-icon.png')} />

在 Flutter 中,如果要增加静态图片的话就在 widget 的 build 方法中使用 AssetImage 类。

In Flutter, add a static image to your app using the AssetImage class in a widget’s build method.

image: AssetImage('assets/background.png'),

如果想了解更多相关信息,请参考文档 在 Flutter 中添加资源和图片

For more information, see Adding Assets and Images in Flutter.

如何在网络中加载图片?

How do I load images over a network?

在 React Native,你可以在 Imagesource 属性中设置 uri 和所需的尺寸。

In React Native, you would specify the uri in the source prop of the Image component and also provide the size if needed.

在 Flutter 中,使用 Image.network 构造函数来实现通过地址加载图片的操作。

In Flutter, use the Image.network constructor to include an image from a URL.

// Flutter
body: Image.network(
          'https://flutter.io/images/owl.jpg',

我如何安装依赖包和包插件?

How do I install packages and package plugins?

Flutter 支持使用开发者向 Flutter 和 Dart 生态系统贡献的代码包。这样可以使大量开发者快速构建应用程序而无需重复造车轮。而平台相关的代码包就被称为包插件。

Flutter supports using shared packages contributed by other developers to the Flutter and Dart ecosystems. This allows you to quickly build your app without having to develop everything from scratch. Packages that contain platform-specific code are known as package plugins.

在 React Native 中,你可以在命令行中运行 yarn add {package-name} 或者 npm install --save {package-name} 来安装代码包。

In React Native, you would use yarn add {package-name} or npm install --save {package-name} to install packages from the command line.

在 Flutter 中,安装代码包需要按照如下的步骤:

In Flutter, install a package using the following instructions:

  1. pubspec.yaml 的 dependencies 区域添加包名和版本。下面的例子向大家展示了如何将 google_sign_in 的 Dart package 添加到 pubspec.yaml 中。一定要检查一下 YAML 文件中的空格,因为 空格很重要!

    Add the package name and version to the pubspec.yaml dependencies section. The example below shows how to add the google_sign_in Dart package to the pubspec.yaml file. Check your spaces when working in the YAML file because white space matters!

dependencies:
  flutter:
    sdk: flutter
  google_sign_in: ^3.0.3
  1. 在命令行中输入 flutter pub get 来安装代码包。如果使用 IDE,它自己会运行 flutter pub get,或者它会提示你是不是要运行该命令。

    Install the package from the command line by using flutter pub get. If using an IDE, it often runs flutter pub get for you, or it might prompt you to do so.

  2. 向下面代码一样在程序中引用代码包:

    Import the package into your app code as shown below:

import 'package:google_sign_in/google_sign_in.dart';

如果想了解更多相关信息,请参考 在 Flutter 里使用 PackagesFlutter Packages 的开发和提交

For more information, see Using Packages and Developing Packages & Plugins.

你可以找到很多 Flutter 开发者分享的代码包,就在 Flutter packagespub.dev

You can find many packages shared by Flutter developers in the Flutter packages section of the pub.dev.

Flutter widgets

在 Flutter 中,你可以基于 widget 打造你自己的 UI,通过 widget 当前的设置和状态会呈现相应的页面效果。

In Flutter, you build your UI out of widgets that describe what their view should look like given their current configuration and state.

Widget 常常通过很多小的、单一功能的 widget 组成,通过这样的封装往往能够实现很棒的效果。比如, Container widget 包含多种 widget,分别负责布局、绘图、位置变化和尺寸变化。准确的说,Container widget 包括 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform widget。与其继承 Container 来实现自定义效果,不如直接修改这些 widget 来实现效果。

Widgets are often composed of many small, single-purpose widgets that are nested to produce powerful effects. For example, the Container widget consists of several widgets responsible for layout, painting, positioning, and sizing. Specifically, the Container widget includes the LimitedBox, ConstrainedBox, Align, Padding, DecoratedBox, and Transform widgets. Rather than subclassing Container to produce a customized effect, you can compose these and other simple widgets in new and unique ways.

Center widget 是另一个用于控制布局的示例。如果要居中一个 widget,就把它封装到 Center widget 中,然后使用布局 widget 来进行对齐行、列和网格。这些布局 widget 并不可见。而他们的作用就是控制其它 widget 的布局。如果想搞清楚为什么一个 widget 会有这样的效果,有效的方法是研究它临近的 widget。

The Center widget is another example of how you can control the layout. To center a widget, wrap it in a Center widget and then use layout widgets for alignment, row, columns, and grids. These layout widgets do not have a visual representation of their own. Instead, their sole purpose is to control some aspect of another widget’s layout. To understand why a widget renders in a certain way, it’s often helpful to inspect the neighboring widgets.

如果想了解更多相关信息,请参考 技术概览

For more information, see the Flutter Technical Overview.

如果想了解更多关于 Widgets 包中的核心 widget,请参考 基础 Flutter Widgets核心 Widget 目录 或是 Flutter Widget 目录

For more information about the core widgets from the Widgets package, see Flutter Basic Widgets, the Flutter Widget Catalog, or the Flutter Widget Index.

视图

Views

View 等价容器的是什么?

What is the equivalent of the View container?

在 React Native 中, View 是支持 Flexbox 布局、风格化、触摸事件处理和访问性控制的容器。

In React Native, View is a container that supports layout with Flexbox, style, touch handling, and accessibility controls.

在 Flutter 中,你可以使用 Widgets 库中的核心布局 widget,比如 ContainerColumnRowCenter。如果想了解更多相关信息,请参考 布局类 Widgets 目录。

In Flutter, you can use the core layout widgets in the Widgets library, such as Container, Column, Row, and Center. For more information, see the Layout Widgets catalog.

FlatList 或者 SectionList 相对应的是什么?

What is the equivalent of FlatList or SectionList?

List 是一个可以滚动的纵向排列的组件列表。

A List is a scrollable list of components arranged vertically.

在 React Native 中,FlatList 或者 SectionList 用于渲染简单的或者分组的列表。

In React Native, FlatList or SectionList are used to render simple or sectioned lists.

// React Native
<FlatList
  data={[ ... ]}
  renderItem={({ item }) => <Text>{item.key}</Text>}
/>

ListView 是 Flutter 最常用的滑动 widget。默认构造函数需要一个数据列表的参数。 ListView 非常适合用于少量子 widget 的列表。如果列表的元素比较多,可以使用 ListView.builder,它会按需构建子项并且只创建可见的子项。

ListView is Flutter’s most commonly used scrolling widget. The default constructor takes an explicit list of children. ListView is most appropriate for a small number of widgets. For a large or infinite list, use ListView.builder, which builds its children on demand and only builds those children that are visible.

// Flutter
var data = [ ... ];
ListView.builder(
  itemCount: data.length,
  itemBuilder: (context, int index) {
    return Text(
      data[index],
    );
  },
)
Flat list on Android
Android
Flat list on iOS
iOS

如果要了解如何实现无限滑动列表,请参考 Write Your First Flutter App, Part 1 的 codelab。

To learn how to implement an infinite scrolling list, see the Write Your First Flutter App, Part 1 codelab.

如何使用 Canvas 绘图?

How do I use a Canvas to draw or paint?

在 React Native 中,canvas 组件是不可见的,所以需要使用类似 react-native-canvas 这样的组件。

In React Native, canvas components aren’t present so third party libraries like react-native-canvas are used.

// React Native
handleCanvas = canvas => {
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'skyblue';
  ctx.beginPath();
  ctx.arc(75, 75, 50, 0, 2 * Math.PI);
  ctx.fillRect(150, 100, 300, 300);
  ctx.stroke();
};

render() {
  return (
    <View>
      <Canvas ref={this.handleCanvas} />
    </View>
  );
}

在 Flutter 中,你可以使用 CustomPaintCustomPainter 在画布上进行绘制。

In Flutter, you can use the CustomPaint and CustomPainter classes to draw to the canvas.

下面的示例代码展示了如何使用 CustomPaint 进行绘图。它实现了抽象类 CustomPainter,然后将它赋值给 CustomPainter 的 painter 属性。 CustomPainter 子类必须实现 paintshouldRepaint 方法。

The following example shows how to draw during the paint phase using the CustomPaint widget. It implements the abstract class, CustomPainter, and passes it to CustomPaint’s painter property. CustomPaint subclasses must implement the paint() and shouldRepaint() methods.

// Flutter
class MyCanvasPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    paint.color = Colors.amber;
    canvas.drawCircle(Offset(100.0, 200.0), 40.0, paint);
    Paint paintRect = Paint();
    paintRect.color = Colors.lightBlue;
    Rect rect = Rect.fromPoints(Offset(150.0, 300.0), Offset(300.0, 400.0));
    canvas.drawRect(rect, paintRect);
  }

  bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
  bool shouldRebuildSemantics(MyCanvasPainter oldDelegate) => false;
}
class _MyCanvasState extends State<MyCanvas> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomPaint(
        painter: MyCanvasPainter(),
      ),
    );
  }
}
Canvas on Android
Android
Canvas on iOS
iOS

布局

Layouts

如何使用 widget 来定义布局属性?

How do I use widgets to define layout properties?

在 React Native 中,大多数布局需要通过向指定的组件传递属性参数进行设置。比如,你可以使用 Viewstyle 来设置 flexbox 属性。如果要整理一列的组件,你可以使用如下的属性设置:flexDirection: “column”

In React Native, most of the layout can be done with the props that are passed to a specific component. For example, you could use the style prop on the View component in order to specify the flexbox properties. To arrange your components in a column, you would specify a prop such as: flexDirection: “column”.

// React Native
<View
  style={{
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}
>

在 Flutter 中,布局主要是由专门的 widget 定义的,它们同控制类 widget 和样式属性一起发挥功能。

In Flutter, the layout is primarily defined by widgets specifically designed to provide layout, combined with control widgets and their style properties.

比如,ColumnRow widget 接受一个数组的子元素并且分别按照纵向和横向进行排列。 Container widget 包含布局和样式属性的组合, Center widget 会将其自 widget 也设定居中。

For example, the Column and Row widgets take an array of children and align them vertically and horizontally respectively. A Container widget takes a combination of layout and styling properties, and a Center widget centers its child widgets.

// Flutter
Center(
  child: Column(
    children: <Widget>[
      Container(
        color: Colors.red,
        width: 100.0,
        height: 100.0,
      ),
      Container(
        color: Colors.blue,
        width: 100.0,
        height: 100.0,
      ),
      Container(
        color: Colors.green,
        width: 100.0,
        height: 100.0,
      ),
    ],
  ),
)

Flutter 在核心 widget 库中提供多种不同的布局 widget。比如 PaddingAlignStack

Flutter provides a variety of layout widgets in its core widget library. For example, Padding, Align, and Stack.

要得到完整的 widget 列表,请参考 Layout Widgets

For a complete list, see Layout Widgets.

Layout on Android
Android
Layout on iOS
iOS

如何为 widget 分层?

How do I layer widgets?

在 React Native 中,组件可以通过 absolute 划分层次。

In React Native, components can be layered using absolute positioning.

在 Flutter 中使用 Stack widget 将子 widget 进行分层。该 widget 可以将整体或者部分的子 widget 进行分层。

Flutter uses the Stack widget to arrange children widgets in layers. The widgets can entirely or partially overlap the base widget.

Stack widget 将子 widget 根据容器的边界进行布局。如果你仅仅想把子 widget 重叠摆放的话,这个 widget 非常合适。

The Stack widget positions its children relative to the edges of its box. This class is useful if you simply want to overlap several children widgets.

// Flutter
Stack(
  alignment: const Alignment(0.6, 0.6),
  children: <Widget>[
    CircleAvatar(
      backgroundImage: NetworkImage(
        'https://avatars3.githubusercontent.com/u/14101776?v=4'),
    ),
    Container(
      decoration: BoxDecoration(
          color: Colors.black45,
      ),
      child: Text('Flutter'),
    ),
  ],
)

上面的示例代码使用 Stack 将一个 Container (将 Text 显示在一个半透明的黑色背景上)覆盖在一个 CircleAvatar 上。Stack 使用对齐属性和 Alignment 坐标微调文本。

The previous example uses Stack to overlay a Container (that displays its Text on a translucent black background) on top of a CircleAvatar. The Stack offsets the text using the alignment property and Alignment coordinates.

Stack on Android
Android
Stack on iOS
iOS

如果想了解更多相关信息,请参考 Stack 类的文档。

For more information, see the Stack class documentation.

风格化

Styling

如何设置组件的风格?

How do I style my components?

在 React Native 中,内联风格化和 stylesheets.create 可以用于设置组件的风格。

In React Native, inline styling and stylesheets.create are used to style components.

// React Native
<View style={styles.container}>
  <Text style={{ fontSize: 32, color: 'cyan', fontWeight: '600' }}>
    This is a sample text
  </Text>
</View>

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

在 Flutter 中, Text widget 可以接受 TextStyle 作为它的风格化属性。如果你想在不同的场合使用相同的文本风格,你可以创建一个 TextStyle 类,并且在多个 Text widget 中使用它。

In Flutter, a Text widget can take a TextStyle class for its style property. If you want to use the same text style in multiple places, you can create a TextStyle class and use it for multiple Text widgets.

// Flutter
var textStyle = TextStyle(fontSize: 32.0, color: Colors.cyan, fontWeight:
   FontWeight.w600);
	...
Center(
  child: Column(
    children: <Widget>[
      Text(
        'Sample text',
        style: textStyle,
      ),
      Padding(
        padding: EdgeInsets.all(20.0),
        child: Icon(Icons.lightbulb_outline,
          size: 48.0, color: Colors.redAccent)
      ),
    ],
  ),
)
Styling on Android
Android
Styling on iOS
iOS

我如何使用 IconsColors 呢?

How do I use Icons and Colors?

React Native 并不包含默认图标,所以需要使用第三方库。

React Native doesn’t include support for icons so third party libraries are used.

在 Flutter 中,引用 Material 库的时候就同时引入了 Material iconscolors

In Flutter, importing the Material library also pulls in the rich set of Material icons and colors.

Icon(Icons.lightbulb_outline, color: Colors.redAccent)

当使用 Icons 类时,确保在项目的 pubspec.yaml 文件中设置 uses-material-design: true,这样保证 MaterialIcons 相关字体被包含在你的应用中。一般来说,如果你想用 Material 库的话,则需要包含这一行内容。

When using the Icons class, make sure to set uses-material-design: true in the project’s pubspec.yaml file. This ensures that the MaterialIcons font, which displays the icons, is included in your app. In general, if you intend to use the Material library, you should include this line.

name: my_awesome_application
flutter: uses-material-design: true

Flutter 的 Cupertino (iOS-style) package 为 iOS 设计语言提供高分辨率的 widget。要使用 CupertinoIcons 字体,在项目的 pubspec.yaml 文件中添加 cupertino_icons 的依赖即可。

Flutter’s Cupertino (iOS-style) package provides high fidelity widgets for the current iOS design language. To use the CupertinoIcons font, add a dependency for cupertino_icons in your project’s pubspec.yaml file.

name: my_awesome_application
dependencies:
  cupertino_icons: ^0.1.0

要在全局范围内自定义组件的颜色和风格,使用 ThemeData 为不同的主题指定默认颜色。在 MaterialApp 的主题属性中设置 ThemeData 对象。 Colors 类提供 Material Design color palette 中所提供的颜色配置。

To globally customize the colors and styles of components, use ThemeData to specify default colors for various aspects of the theme. Set the theme property in MaterialApp to the ThemeData object. The Colors class provides colors from the Material Design color palette.

下面的示例代码将主色调设置为 blue 然后文本颜色设置为 red

The following example sets the primary swatch to blue and the text selection to red.

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}

如何增加风格化主题?

How do I add style themes?

在 React Native,常用主题都定义在样式层叠表中。

In React Native, common themes are defined for components in stylesheets and then used in components.

在 Flutter 中,为所有组件创建统一风格可以在 ThemeData 类中定义,并将它赋值给 MaterialApp 的主题属性。

In Flutter, create uniform styling for almost everything by defining the styling in the ThemeData class and passing it to the theme property in the MaterialApp widget.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.cyan,
        brightness: Brightness.dark,
      ),
      home: StylingPage(),
    );
  }

Theme 可以在不使用 MaterialApp widget 的情况下使用。 Theme 接受一个 ThemeData 参数,并且将 ThemeData 应用于它的全部子 widget。

A Theme can be applied even without using the MaterialApp widget. The Theme widget takes a ThemeData in its data parameter and applies the ThemeData to all of its children widgets.

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData(
        primaryColor: Colors.cyan,
        brightness: brightness,
      ),
      child: Scaffold(
         backgroundColor: Theme.of(context).primaryColor,
              ...
              ...
      ),
    );
  }

状态管理

State management

当 widget 被创建或者在 widget 的生命周期中有信息发生改变时所产生的信息叫做状态。要在 Flutter 中管理应用程序的状态,使用 StatefulWidget 和 State 对象。

State is information that can be read synchronously when a widget is built or information that might change during the lifetime of a widget. To manage app state in Flutter, use a StatefulWidget paired with a State object.

欲知更多关于 Flutter 的状态管理相关的内容,请参访 状态管理文档 页面。

For more information on ways to approach managing state in Flutter, see State management.

The StatelessWidget

StatelessWidget widget

StatelessWidget 在 Flutter 中是一个不需要状态改变的 widget,它没有内部的状态。

A StatelessWidget in Flutter is a widget that doesn’t require a state change—it has no internal state to manage.

当你展现给用户的界面并不依赖其它任何配置信息并且使用 BuildContext 来解析 widget,则需要使用无状态 widget。

Stateless widgets are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object itself and the BuildContext in which the widget is inflated.

AboutDialogCircleAvatarTextStatelessWidget 的子类,并且是很典型的无状态 widget。

AboutDialog, CircleAvatar, and Text are examples of stateless widgets that subclass StatelessWidget.

// Flutter
import 'package:flutter/material.dart';

void main() => runApp(MyStatelessWidget(text: 'StatelessWidget Example to show immutable data'));

class MyStatelessWidget extends StatelessWidget {
  final String text;
  MyStatelessWidget({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        text,
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

在上面的例子中,你用到了 MyStatelessWidget 类的构造函数来传递 text。并且它被标记为 final。该类继承了 StatelessWidget,它包含不可变的数据。

The previous example uses the constructor of the MyStatelessWidget class to pass the text, which is marked as final. This class extends StatelessWidget—it contains immutable data.

无状态 widget 的 build 方法通常只有在三种情况下会被调用:

The build method of a stateless widget is typically called in only three situations:

The StatefulWidget

StatefulWidget widget

StatefulWidget 是携带状态变化的 widget。通过调用 setState 方法可以管理 StatefulWidget 的状态。当调用 setState() 的时候,程序会通知 Flutter 框架有状态发生了改变,然后会重新运行 build() 方法来更新应用的状态。

A StatefulWidget is a widget that changes state. Use the setState method to manage the state changes for a StatefulWidget. A call to setState() tells the Flutter framework that something has changed in a state, which causes an app to rerun the build() method so that the app can reflect the change.

状态 是在 widget 被创建期间可以被同步读取的信息,并且在 widget 的生命周期中会发生改变。实现该 widget 的时候要注意保证党状态发生改变的时候程序能够获得相应的提醒。当 widget 能够动态改变的时候,请使用 StatefulWidget。比如,某个 widget 会随着用户填写表单或者移动滑块的时候发生改变。亦或者随着数据源更新的时候发生改变。

State is information that can be read synchronously when a widget is built and might change during the lifetime of the widget. It’s the responsibility of the widget implementer to ensure that the state object is promptly notified when the state changes. Use StatefulWidget when a widget can change dynamically. For example, the state of the widget changes by typing into a form, or moving a slider. Or, it can change over time—perhaps a data feed updates the UI.

CheckboxRadioSliderInkWellForm、和 TextField 都是有状态的 widget,是 StatefulWidget 的子类。

Checkbox, Radio, Slider, InkWell, Form, and TextField are examples of stateful widgets that subclass StatefulWidget.

下面的示例代码声明了一个 StatefulWidget,需要实现 createState() 方法。该方法创建一个对象来管理 widget 的状态,也就是 _MyStatefulWidgetState

The following example declares a StatefulWidget that requires a createState() method. This method creates the state object that manages the widget’s state, _MyStatefulWidgetState.

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

下面的状态类,_MyStatefulWidgetState,实现了 build() 方法。当状态发生改变的时候,比如说用户点击了开关按钮,这时 setState 就会被调用,并且将新的开关状态传进来。这就会使整体框架重构这个 widget。

The following state class, _MyStatefulWidgetState, implements the build() method for the widget. When the state changes, for example, when the user toggles the button, setState() is called with the new toggle value. This causes the framework to rebuild this widget in the UI.

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  bool showtext=true;
  bool toggleState=true;
  Timer t2;

  void toggleBlinkState(){
    setState((){
      toggleState=!toggleState;
    });
    var twenty = const Duration(milliseconds: 1000);
    if(toggleState==false) {
      t2 = Timer.periodic(twenty, (Timer t) {
        toggleShowText();
      });
    } else {
      t2.cancel();
    }
  }

  void toggleShowText(){
    setState((){
      showtext=!showtext;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            (showtext
              ?(Text('This execution will be done before you can blink.'))
              :(Container())
            ),
            Padding(
              padding: EdgeInsets.only(top: 70.0),
              child: ElevatedButton(
                onPressed: toggleBlinkState,
                child: (toggleState
                  ?( Text('Blink'))
                  :(Text('Stop Blinking'))
                )
              )
            )
          ],
        ),
      ),
    );
  }
}

StatefulWidget 和 StatelessWidget 的最佳实践是什么?

What are the StatefulWidget and StatelessWidget best practices?

下面有一些设计原则供大家参考。

Here are a few things to consider when designing your widget.

  1. 确定一个 widget 应该是 StatefulWidget 还是 StatelessWidget

    Determine whether a widget should be a StatefulWidget or a StatelessWidget

在 Flutter 中, widget 要么是有状态的,要么是无状态的。这取决于 widget 是否依赖状态的改变。

In Flutter, widgets are either Stateful or Stateless—depending on whether they depend on a state change.

  1. 确定哪个对象来控制 widget 的状态(针对 StatefulWidget)。

    Determine which object manages the widget’s state (for a StatefulWidget)

在 Flutter 中,有三种途径来管理状态:

In Flutter, there are three primary ways to manage state:

当决定了使用哪个途径后,要考虑下述的几个原则:

When deciding which approach to use, consider the following principles:

  1. 继承 StatefulWidget 和状态

    Subclass StatefulWidget and State.

MyStatefulWidget 类管理它自身的状态—&mdash它继承自 StatefulWidget,重写了 createState() 方法。该方法创建了 State 对象,同时框架会调用 createState() 方法来构建 widget。在这个例子中,createState() 方法创建了一个 _MyStatefulWidgetState 实例。下面的最佳实践中也实现了类似的方法。

The MyStatefulWidget class manages its own state—it extends StatefulWidget, it overrides the createState() method to create the State object, and the framework calls createState() to build the widget. In this example, createState() creates an instance of _MyStatefulWidgetState, which is implemented in the next best practice.

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

  @override
  Widget build(BuildContext context) {
    ...
  }
}
  1. 将 StatefulWidget 添加到 widget 树中

    Add the StatefulWidget into the widget tree.

将你自定义的 StatefulWidget 通过应用程序的 build 方法添加到 widget 树中。

Add your custom StatefulWidget to the widget tree in the app’s build method.

class MyStatelessWidget extends StatelessWidget {
  // This widget is the root of your application.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyStatefulWidget(title: 'State Change Demo'),
    );
  }
}
State change on Android
Android
State change on iOS
iOS

Props

在 React Native 中,大多数组件都可以在创建的时候通过不同的参数或者属性来自定义,叫做 props。这些参数可以在子组件中通过 this.props 进行调用。

In React Native, most components can be customized when they are created with different parameters or properties, called props. These parameters can be used in a child component using this.props.

// React Native
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title='Press'
          onPress={() => this.props.onPress(this.props.index)}
        />
      </View>
    );
  }
}
class App extends React.Component {

  onPress = index => {
    console.log('Card ', index);
  };

  render() {
    return (
      <View>
        <FlatList
          data={[ ... ]}
          renderItem={({ item }) => (
            <CustomCard onPress={this.onPress} index={item.key} />
          )}
        />
      </View>
    );
  }
}

在 Flutter 中,你可以将构造函数中的参数值赋值给标记为 final 的本地变量或者函数。

In Flutter, you assign a local variable or function marked final with the property received in the parameterized constructor.

// Flutter
class CustomCard extends StatelessWidget {

  CustomCard({@required this.index, @required this.onPress});
  final index;
  final Function onPress;

  @override
  Widget build(BuildContext context) {
  return Card(
    child: Column(
      children: <Widget>[
        Text('Card $index'),
        TextButton(
          child: const Text('Press'),
          onPressed: this.onPress,
        ),
      ],
    ));
  }
}
    ...
//Usage
CustomCard(
  index: index,
  onPress: () {
    print('Card $index');
  },
)
Cards on Android
Android
Cards on iOS
iOS

本地存储

Local storage

如果你不需要在本地存储太多数据同时也不需要存储结构化数据,那么你可以使用 shared_preferences,通过它来读写一些原始数据类型键值对,数据类型包括 boolean、float、ints、longs 和 string。

If you don’t need to store a lot of data and it doesn’t require structure, you can use shared_preferences which allows you to read and write persistent key-value pairs of primitive data types: booleans, floats, ints, longs, and strings.

如何存储在应用程序中全局有效的键值对?

How do I store persistent key-value pairs that are global to the app?

在 React Native,可以使用 AsyncStorage 中的 setItemgetItem 函数来存储和读取应用程序中的全局数据。

In React Native, you use the setItem and getItem functions of the AsyncStorage component to store and retrieve data that is persistent and global to the app.

// React Native
await AsyncStorage.setItem( 'counterkey', json.stringify(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
  if (value != null) {
    this.setState({ counter: value });
  }
});

在 Flutter 中,使用 shared_preferences 插件来存储和访问应用程序内全局有效的键值对数据。 shared_preferences 插件封装了 iOS 中的 NSUserDefaults 和 Android 中的 SharedPreferences 来实现简单数据的持续存储。如果要使用该插件,可以在 pubspec.yaml 中添加依赖 shared_preferences,然后在 Dart 文件中引用包即可。

In Flutter, use the shared_preferences plugin to store and retrieve key-value data that is persistent and global to the app. The shared_preferences plugin wraps NSUserDefaults on iOS and SharedPreferences on Android, providing a persistent store for simple data. To use the plugin, add shared_preferences as a dependency in the pubspec.yaml file then import the package in your Dart file.

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^0.4.3
// Dart
import 'package:shared_preferences/shared_preferences.dart';

要实现持久数据存储,使用 SharedPreferences 类提供的 setter 方法即可。 Setter 方法适用于多种原始类型数据,比如 setInt, setBool, 和 setString。要读取数据,使用 SharedPreferences 类中相应的 getter 方法。每一个 setter 方法都有对应的 getter 方法,比如,getInt, getBool, 和 getString

To implement persistent data, use the setter methods provided by the SharedPreferences class. Setter methods are available for various primitive types, such as setInt, setBool, and setString. To read data, use the appropriate getter method provided by the SharedPreferences class. For each setter there is a corresponding getter method, for example, getInt, getBool, and getString.

SharedPreferences prefs = await SharedPreferences.getInstance();
_counter = prefs.getInt('counter');
prefs.setInt('counter', ++_counter);
setState(() {
  _counter = _counter;
});

路径

Routing

大多数应用都会包含多个页面来显示不同类型的数据。比如,你有一个页面展示商品列表,用户可以通过点击其中的任意一个商品,在另外一个页面查看该商品的详细信息。

Most apps contain several screens for displaying different types of information. For example, you might have a product screen that displays images where users could tap on a product image to get more information about the product on a new screen.

在 Android 中,新的页面是 Activity。在 iOS 中,新的页面是 ViewController。在 Flutter 中,页面也只是 widget,如果在 Flutter 中要切换页面,使用 Navigator widget 即可。

In Android, new screens are new Activities. In iOS, new screens are new ViewControllers. In Flutter, screens are just Widgets! And to navigate to new screens in Flutter, use the Navigator widget.

如何在页面之间进行切换?

How do I navigate between screens?

在 React Native,有三种主要的导航 widget : StackNavigator、TabNavigator 和 DrawerNavigator。每个都提供了配置和定义页面的方法。

In React Native, there are three main navigators: StackNavigator, TabNavigator, and DrawerNavigator. Each provides a way to configure and define the screens.

// React Native
const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);
const SimpleApp = StackNavigator({
  Home: { screen: MyApp },
  stackScreen: { screen: StackScreen }
});
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,有两种主要的 widget 实现页面之间的切换:

In Flutter, there are two main widgets used to navigate between screens:

Navigator 以堆栈的方式管理子 widget。它的堆栈里存储的是 Route 对象,并且提供方法管理整个堆栈,比如 Navigator.pushNavigator.pop。路径列表需要在 MaterialApp 中指定。或者在页面切换的时候进行构建,比如 hero 动画。下面的例子在 MaterialApp widget 中指定了页面切换路径。

A Navigator is defined as a widget that manages a set of child widgets with a stack discipline. The navigator manages a stack of Route objects and provides methods for managing the stack, like Navigator.push and Navigator.pop. A list of routes might be specified in the MaterialApp widget, or they might be built on the fly, for example, in hero animations. The following example specifies named routes in the MaterialApp widget.

// Flutter
class NavigationApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
            ...
      routes: <String, WidgetBuilder>{
        '/a': (BuildContext context) => usualNavscreen(),
        '/b': (BuildContext context) => drawerNavscreen(),
      }
            ...
  );
  }
}

要切换到一个已命名的路径,Navigator.of() 方法被用于指定 BuildContext(该对象可以定位到 widget 树中的一个具体的 widget)。路径的名称传递到 pushNamed 函数来切换至指定的路径。

To navigate to a named route, the Navigator.of() method is used to specify the BuildContext (a handle to the location of a widget in the widget tree). The name of the route is passed to the pushNamed function to navigate to the specified route.

Navigator.of(context).pushNamed('/a');

你可以使用 Navigator 中的 push 方法添加 Route 到 navigator 的历史队列中,其中包含 BuildContext 并且可以切换到指定页面。在下面的例子中,MaterialPageRoute widget 是一个模式化路径,可以将整个页面通过平台自适应切换方式进行切换。它需要一个 WidgetBuilder 参数。

You can also use the push method of Navigator which adds the given Route to the history of the navigator that most tightly encloses the given BuildContext, and transitions to it. In the following example, the MaterialPageRoute widget is a modal route that replaces the entire screen with a platform-adaptive transition. It takes a WidgetBuilder as a required parameter.

Navigator.push(context, MaterialPageRoute(builder: (BuildContext context)
 => UsualNavscreen()));

如何使用 tab 导航和 drawer 导航?

How do I use tab navigation and drawer navigation?

在 Material Design 应用程序中,Flutter 的导航形式主要有两种:tab 和 drawer。如果没有足够的 widget 可以容纳 tab,drawer 就是个不错的选择。

In Material Design apps, there are two primary options for Flutter navigation: tabs and drawers. When there is insufficient space to support tabs, drawers provide a good alternative.

Tab 导航

Tab navigation

在 React Native 中,createBottomTabNavigatorTabNavigation 用来显示 tab 和 tab 导航。

In React Native, createBottomTabNavigator and TabNavigation are used to show tabs and for tab navigation.

// React Native
import { createBottomTabNavigator } from 'react-navigation';

const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);

Flutter 针对 drawer 和 tab 导航提供几种专用的 widget:

Flutter provides several specialized widgets for drawer and tab navigation:

// Flutter
TabController controller=TabController(length: 2, vsync: this);

TabBar(
  tabs: <Tab>[
    Tab(icon: Icon(Icons.person),),
    Tab(icon: Icon(Icons.email),),
  ],
  controller: controller,
),

要将 tab 选项与 TabBarTabBarView 结合起来使用就需要 TabControllerTabController 的构造函数中的 length 参数定义了 tab 的总数。当状态变化时,需要使用 TickerProvider 来触发通知。 TickerProvidervsync,当你需要创建新的 TabController 时,将 vsync: this 作为构造函数的参数即可。

A TabController is required to coordinate the tab selection between a TabBar and a TabBarView. The TabController constructor length argument is the total number of tabs. A TickerProvider is required to trigger the notification whenever a frame triggers a state change. The TickerProvider is vsync. Pass the vsync: this argument to the TabController constructor whenever you create a new TabController.

TickerProvider 接口可以用于生成 Ticker 对象。当有对象被触发通知后会用到 Tickers,不过它通常都是被 AnimationController 间接调用。 AnimationControllers 需要 TickerProvider 来获得对应的 Ticker。如果你通过 State 创建了一个 AnimationController,那么你就可以使用 TickerProviderStateMixin 或者 SingleTickerProviderStateMixin 来获得对应的 TickerProvider

The TickerProvider is an interface implemented by classes that can vend Ticker objects. Tickers can be used by any object that must be notified whenever a frame triggers, but they’re most commonly used indirectly via an AnimationController. AnimationControllers need a TickerProvider to obtain their Ticker. If you are creating an AnimationController from a State, then you can use the TickerProviderStateMixin or SingleTickerProviderStateMixin classes to obtain a suitable TickerProvider.

Scaffold 封装了一个新的 TabBar widget,其中包含两个 tab。 TabBarView 作为 body 参数传递到 Scaffold 中。所有和 TabBar 中的 tab 相关的页面均是 TabBarView 的子 widget,并且都对应同一个 TabController

The Scaffold widget wraps a new TabBar widget and creates two tabs. The TabBarView widget is passed as the body parameter of the Scaffold widget. All screens corresponding to the TabBar widget’s tabs are children to the TabBarView widget along with the same TabController.

// Flutter

class _NavigationHomePageState extends State<NavigationHomePage> with SingleTickerProviderStateMixin {
  TabController controller=TabController(length: 2, vsync: this);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: Material (
        child: TabBar(
          tabs: <Tab> [
            Tab(icon: Icon(Icons.person),)
            Tab(icon: Icon(Icons.email),),
          ],
          controller: controller,
        ),
        color: Colors.blue,
      ),
      body: TabBarView(
        children: <Widget> [
          home.homeScreen(),
          tabScreen.tabScreen()
        ],
        controller: controller,
      )
    );
  }
}

Drawer 导航

Drawer navigation

在 React Native 中,导入所需的 react-navigation 包,然后使用 createDrawerNavigatorDrawerNavigation 实现。

In React Native, import the needed react-navigation packages and then use createDrawerNavigator and DrawerNavigation.

// React Native
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,我们可以结合 DrawerScaffold 一起使用来实现 Material Design 风格的 drawer 布局。如果要在应用程序中添加 Drawer,可以将它封装在 Scaffold widget 中。 Scaffold widget 提供了一种一致的界面风格,它遵循 Material Design 的设计原则。同时它还支持一些特殊的 Material Design 组件,比如 DrawersAppBars,和 SnackBars

In Flutter, we can use the Drawer widget in combination with a Scaffold to create a layout with a Material Design drawer. To add a Drawer to an app, wrap it in a Scaffold widget. The Scaffold widget provides a consistent visual structure to apps that follow the Material Design guidelines. It also supports special Material Design components, such as Drawers, AppBars, and SnackBars.

Drawer 就是一个 Material Design 窗格,它可以从 Scaffold 边缘水平滑动显示应用程序的导航选项。你可以在里面添加 ElevatedButtonText 。或者添加一个列表的元素作为 Drawer 的子 widget。在下面的例子中,ListTile 提供了点击导航。

The Drawer widget is a Material Design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application. You can provide a ElevatedButton, a Text widget, or a list of items to display as the child to the Drawer widget. In the following example, the ListTile widget provides the navigation on tap.

// Flutter
Drawer(
  child:ListTile(
    leading: Icon(Icons.change_history),
    title: Text('Screen2'),
    onTap: () {
      Navigator.of(context).pushNamed('/b');
    },
  ),
  elevation: 20.0,
),

Scaffold 还包含一个 AppBar。它会自动显示一个图标按钮来表明 Scaffold 中有一个DrawerScaffold 会自动处理边缘的滑动手势来显示 Drawer

The Scaffold widget also includes an AppBar widget that automatically displays an appropriate IconButton to show the Drawer when a Drawer is available in the Scaffold. The Scaffold automatically handles the edge-swipe gesture to show the Drawer.

// Flutter
@override
Widget build(BuildContext context) {
  return Scaffold(
    drawer: Drawer(
      child: ListTile(
        leading: Icon(Icons.change_history),
        title: Text('Screen2'),
        onTap: () {
          Navigator.of(context).pushNamed('/b');
        },
      ),
      elevation: 20.0,
    ),
    appBar: AppBar(
      title: Text('Home'),
    ),
    body: Container(),
  );
}
Navigation on Android
Android
Navigation on iOS
iOS

手势检测和触摸事件处理

Gesture detection and touch event handling

Flutter 支持点击、拖拽和缩放手势来监听和相应手势操作。 Flutter 中的手势处理有两个独立的层。第一层是指针事件,指针事件定义了指针在屏幕上的位置和动作,比如触摸、鼠标和触摸笔。第二层指手势,主要是语义层面的动作,里面包含一种或者多种指针动作。

To listen for and respond to gestures, Flutter supports taps, drags, and scaling. The gesture system in Flutter has two separate layers. The first layer includes raw pointer events, which describe the location and movement of pointers, (such as touches, mice, and styli movements), across the screen. The second layer includes gestures, which describe semantic actions that consist of one or more pointer movements.

如何为 widget 添加点击或者按压的监听器?

How do I add a click or press listeners to a widget?

在 React Native 中,使用 PanResponder 或者 Touchable 组件来添加监听器。

In React Native, listeners are added to components using PanResponder or the Touchable components.

// React Native
<TouchableOpacity
  onPress={() => {
    console.log('Press');
  }}
  onLongPress={() => {
    console.log('Long Press');
  }}
>
  <Text>Tap or Long Press</Text>
</TouchableOpacity>

对于更加复杂手势以及将多个触摸添加到单独的一个手势中,可以使用 PanResponder

For more complex gestures and combining several touches into a single gesture, PanResponder is used.

// React Native
class App extends Component {

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onMoveShouldSetPanResponder: (event, gestureState) =>
        !!getDirection(gestureState),
      onPanResponderMove: (event, gestureState) => true,
      onPanResponderRelease: (event, gestureState) => {
        const drag = getDirection(gestureState);
      },
      onPanResponderTerminationRequest: (event, gestureState) => true
    });
  }

  render() {
    return (
      <View style={styles.container} {...this._panResponder.panHandlers}>
        <View style={styles.center}>
          <Text>Swipe Horizontally or Vertically</Text>
        </View>
      </View>
    );
  }
}

在 Flutter 中,要为 widget 添加点击或者按压监听器,使用带有 onPress: field 的按钮或者可触摸 widget 即可。或者,用任何 widget 封装 GestureDetector,在其中添加手势检测。

In Flutter, to add a click (or press) listener to a widget, use a button or a touchable widget that has an onPress: field. Or, add gesture detection to any widget by wrapping it in a GestureDetector.

// Flutter
GestureDetector(
  child: Scaffold(
    appBar: AppBar(
      title: Text('Gestures'),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Tap, Long Press, Swipe Horizontally or Vertically '),
        ],
      )
    ),
  ),
  onTap: () {
    print('Tapped');
  },
  onLongPress: () {
    print('Long Pressed');
  },
  onVerticalDragEnd: (DragEndDetails value) {
    print('Swiped Vertically');
  },
  onHorizontalDragEnd: (DragEndDetails value) {
    print('Swiped Horizontally');
  },
);

如果想要了解更多详细内容,包括 Flutter 的 GestureDetector 回调函数的列表,请查看页面 GestureDetector 类

For more information, including a list of Flutter GestureDetector callbacks, see the GestureDetector class.

Gestures on Android
Android
Gestures on iOS
iOS

发起 HTTP 网络请求

Making HTTP network requests

对于大多数应用程序来说都需要从互联网上获取数据。在 Flutter 中,http 包提供了从互联网获取数据的最简单的途径。

Fetching data from the internet is common for most apps. And in Flutter, the http package provides the simplest way to fetch data from the internet.

如何通过 API 调用来获得数据呢?

How do I fetch data from API calls?

React Native 提供 Fetch API 实现网络编程,你可以发起请求,然后接收响应来获得数据。

React Native provides the Fetch API for networking—you make a fetch request and then receive the response to get the data.

// React Native
_getIPAddress = () => {
  fetch('https://httpbin.org/ip')
    .then(response => response.json())
    .then(responseJson => {
      this.setState({ _ipAddress: responseJson.origin });
    })
    .catch(error => {
      console.error(error);
    });
};

Flutter 使用 http 包。如果要安装 http 包,将它添加到 pubspec.yaml 的 dependencies 部分。

Flutter uses the http package. To install the http package, add it to the dependencies section of our pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter
  http: <latest_version>

Flutter 使用 dart:io 提供核心的 HTTP 客户端支持,要创建一个 HTTP 客户端,引用 dart:io

Flutter uses the dart:io core HTTP support client. To create an HTTP Client, import dart:io.

import 'dart:io';

客户端支持如下所列的 HTTP 操作:GET, POST, PUT 和 DELETE。

The client supports the following HTTP operations: GET, POST, PUT, and DELETE.

// Flutter
final url = Uri.parse('https://httpbin.org/ip');
final httpClient = HttpClient();
_getIPAddress() async {
  var request = await httpClient.getUrl(url);
  var response = await request.close();
  var responseBody = await response.transform(utf8.decoder).join();
  String ip = jsonDecode(responseBody)['origin'];
  setState(() {
    _ipAddress = ip;
  });
}
API calls on Android
Android
API calls on iOS
iOS

输入表单

Form input

TextField 用于在应用程序中输入文本,这样就可以实现创建表单、短消息应用、搜索框等等功能。Flutter 提供两个核心文本输入 widget: TextFieldTextFormField.

Text fields allow users to type text into your app so they can be used to build forms, messaging apps, search experiences, and more. Flutter provides two core text field widgets: TextField and TextFormField.

如何使用文本输入 widget ?

How do I use text field widgets?

在 React Native 里,可以使用 TextInput 组件来输入文本,它会显示一个输入框,然后通过回调函数来传递输入值。

In React Native, to enter text you use a TextInput component to show a text input box and then use the callback to store the value in a variable.

// React Native
<TextInput
  placeholder="Enter your Password"
  onChangeText={password => this.setState({ password })}
 />
<Button title="Submit" onPress={this.validate} />

在 Flutter 中,使用 TextEditingController 类来管理 TextField widget。当用户修改文本的时候,controller 会通知监听器。

In Flutter, use the TextEditingController class to manage a TextField widget. Whenever the text field is modified, the controller notifies its listeners.

监听器读取文本和选项属性来获知用户所输入的内容。你可以通过 TextField 中的 text 属性获得用户输入的文本数据。

Listeners read the text and selection properties to learn what the user typed into the field. You can access the text in TextField by the text property of the controller.

// Flutter
final TextEditingController _controller = TextEditingController();
      ...
TextField(
  controller: _controller,
  decoration: InputDecoration(
    hintText: 'Type something', labelText: 'Text Field '
  ),
),
ElevatedButton(
  child: Text('Submit'),
  onPressed: () {
    showDialog(
      context: context,
        child: AlertDialog(
          title: Text('Alert'),
          content: Text('You typed ${_controller.text}'),
        ),
     );
   },
 ),
)

在这个例子中,当用户点击提交按钮的时候,会弹出窗口显示当前输入的文本内容。可以使用 alertDialog widget 显示提示信息, TextField 的文本通过 text 属性来获得,该属性属于 TextEditingController

In this example, when a user clicks on the submit button an alert dialog displays the current text entered in the text field. This is achieved using an alertDialog widget that displays the alert message, and the text from the TextField is accessed by the text property of the TextEditingController.

如何使用 Form widget 呢?

How do I use Form widgets?

在 Flutter 中,当需要使用带有提交按钮和 TextFormField 组件的复合 widget 时,就会用到 FormTextFormField 内含一个 onSaved 参数,它可以设置一个回调函数,当表单存储的时候会回调该函数。 FormState 用于存储、重置或者验证 Form 内含的每个 FormField。你可以通过将当前表单的 context 属性赋值给 Form.of 来获得 FormState。或者在表单的构造函数里使用 GlobalKey,然后调用 GlobalKey.currentState 来获得 FormState

In Flutter, use the Form widget where TextFormField widgets along with the submit button are passed as children. The TextFormField widget has a parameter called onSaved that takes a callback and executes when the form is saved. A FormState object is used to save, reset, or validate each FormField that is a descendant of this Form. To obtain the FormState, you can use Form.of() with a context whose ancestor is the Form, or pass a GlobalKey to the Form constructor and call GlobalKey.currentState().

final formKey = GlobalKey<FormState>();

...

Form(
  key:formKey,
  child: Column(
    children: <Widget>[
      TextFormField(
        validator: (value) => !value.contains('@') ? 'Not a valid email.' : null,
        onSaved: (val) => _email = val,
        decoration: const InputDecoration(
          hintText: 'Enter your email',
          labelText: 'Email',
        ),
      ),
      ElevatedButton(
        onPressed: _submit,
        child: Text('Login'),
      ),
    ],
  ),
)

下面的示例代码展示了 Form.save()formKey (这个实际上是 GlobalKey)如何被用于表单提交的。

The following example shows how Form.save() and formKey (which is a GlobalKey) are used to save the form on submit.

void _submit() {
  final form = formKey.currentState;
  if (form.validate()) {
    form.save();
    showDialog(
      context: context,
      child: AlertDialog(
        title: Text('Alert'),
        content: Text('Email: $_email, password: $_password'),
      )
    );
  }
}
Input on Android
Android
Input on iOS
iOS

平台相关代码

Platform-specific code

当构建跨平台应用程序的时候,你会尽量多地复用代码。然而,根据不同的应用场景,代码会根据平台的不同有所变化。这就需要提前声明具体的平台来进行独立的实现。

When building a cross-platform app, you want to re-use as much code as possible across platforms. However, scenarios might arise where it makes sense for the code to be different depending on the OS. This requires a separate implementation by declaring a specific platform.

在 React Native 中,下面的实现代码会被用到:

In React Native, the following implementation would be used:

// React Native
if (Platform.OS === 'ios') {
  return 'iOS';
} else if (Platform.OS === 'android') {
  return 'android';
} else {
  return 'not recognised';
}

而在 Flutter 中,则是下面这样的实现:

In Flutter, use the following implementation:

// Flutter
if (Theme.of(context).platform == TargetPlatform.iOS) {
  return 'iOS';
} else if (Theme.of(context).platform == TargetPlatform.android) {
  return 'android';
} else if (Theme.of(context).platform == TargetPlatform.fuchsia) {
  return 'fuchsia';
} else {
  return 'not recognised ';
}

调试

Debugging

应该使用什么工具调试我的 Flutter 应用?

What tools can I use to debug my app in Flutter?

请使用 DevTools 调试你的 Flutter 和 Dart 应用。

Use the DevTools suite for debugging Flutter or Dart apps.

开发者工具包含了 profiling 构建、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 DevTools 文档。

DevTools includes support for profiling, examining the heap, inspecting the widget tree, logging diagnostics, debugging, observing executed lines of code, debugging memory leaks and memory fragmentation. For more information, see the DevTools documentation.

如何进行热重载?

How do I perform a hot reload?

Flutter 的热重载特性可以帮助你快速便捷地实验、构建 UI 和各种特性以及修复 bug。每次修改代码以后,你只需直接热重载你的应用程序即可,而无需重新进行编译。应用程序会根据你的修改进行相应的更新,而程序原有的状态则会被保留。

Flutter’s Stateful Hot Reload feature helps you quickly and easily experiment, build UIs, add features, and fix bugs. Instead of recompiling your app every time you make a change, you can hot reload your app instantly. The app is updated to reflect your change, and the current state of the app is preserved.

在 React Native 中,iOS 模拟器对应的快捷键是 ⌘R,对应 Android 模拟器的快捷键是点击两次 R。

In React Native, the shortcut is ⌘R for the iOS Simulator and tapping R twice on Android emulators.

在 Flutter 中,如果你使用的是 IntelliJ 或者 Android Studio,可以使用 Save All (⌘s/ctrl-s),或者可以点击工具栏上的 Hot Reload 按钮。如果你是在命令行里使用 flutter run 命令运行的程序,在窗口里输入 r 即可。也可以输入 R 进行彻底的重启。

In Flutter, If you are using IntelliJ IDE or Android Studio, you can select Save All (⌘s/ctrl-s), or you can click the Hot Reload button on the toolbar. If you are running the app at the command line using flutter run, type r in the Terminal window. You can also perform a full restart by typing R in the Terminal window.

如何打开程序里的开发者菜单?

How do I access the in-app developer menu?

在 React Native 中,开发者菜单可以通过摇动设备打开:对于 iOS 模拟器的快捷键是 ⌘D 而 Android 模拟器的快捷键是 ⌘M。

In React Native, the developer menu can be accessed by shaking your device: ⌘D for the iOS Simulator or ⌘M for Android emulator.

在 Flutter 中,如果你使用 IDE,那么可以直接使用 IDE 工具。如果你是通过命令行运行 flutter run 来启动应用程序的,你可以在命令行窗口通过输入 h 来打开菜单,或者参考下面的快捷键说明:

In Flutter, if you are using an IDE, you can use the IDE tools. If you start your application using flutter run you can also access the menu by typing h in the terminal window, or type the following shortcuts:

功能

Action

命令行快捷键

Terminal Shortcut

调试功能和属性

Debug functions and properties

应用程序的 widget 层级

Widget hierarchy of the app

w debugDumpApp()

渲染程序的 widget 树

Rendering tree of the app

t debugDumpRenderTree()

Layers

L debugDumpLayerTree()

无障碍

Accessibility

S (遍历顺序) 或者
U (反转点击测试顺序)

S (traversal order) or
U (inverse hit test order)

debugDumpSemantics()

打开或者关闭 widget 窗口

To toggle the widget inspector

i WidgetsApp. showWidgetInspectorOverride

显示或者隐藏框架线条

To toggle the display of construction lines

p debugPaintSizeEnabled

模拟不同的操作系统

To simulate different operating systems

o defaultTargetPlatform

叠加显示性能参数

To display the performance overlay

P WidgetsApp. showPerformanceOverlay

将截屏保存为 flutter.png

To save a screenshot to flutter. png

s  

退出

To quit

q  

动画

Animation

精美的动画效果会使得 UI 更加直观,可以提升整体视觉效果,使应用显得更加精致,从而提升用户体验。 Flutter 的动画框架使得开发者能够更方便地实现简单和复杂的动画。 Flutter SDK 含有很多 Material Design widget。其中已经包括了标准的动画效果,你可以很方便地自定义这些效果。

Well-designed animation makes a UI feel intuitive, contributes to the look and feel of a polished app, and improves the user experience. Flutter’s animation support makes it easy to implement simple and complex animations. The Flutter SDK includes many Material Design widgets that include standard motion effects and you can easily customize these effects to personalize your app.

在 React Native 中,动画 API 用于创建动画。

In React Native, Animated APIs are used to create animations.

在 Flutter 中,使用 Animation 类和 AnimationController 类实现动画。 Animation 是抽象类,内含其当前的值和它的状态(已完成或者已取消)。 AnimationController 类可以正向或者反向播放动画或者停止动画以及为动画设置特定值来自定义动画。

In Flutter, use the Animation class and the AnimationController class. Animation is an abstract class that understands its current value and its state (completed or dismissed). The AnimationController class lets you play an animation forward or in reverse, or stop animation and set the animation to a specific value to customize the motion.

如何添加一个简单的淡入动画效果?

How do I add a simple fade-in animation?

在下面的 React Native 示例中,有一个动画组件,也就是 FadeInView,它是使用 Animated API 创建的。定义了初始的不透明状态,最终状态和动画切换之间的时间间隔。在 Animated 中添加了动画组件,不透明状态 fadeAnim 映射到我们想要添加动画效果的文本组件上,然后在开始动画的时候调用 start()

In the React Native example below, an animated component, FadeInView is created using the Animated API. The initial opacity state, final state, and the duration over which the transition occurs are defined. The animation component is added inside the Animated component, the opacity state fadeAnim is mapped to the opacity of the Text component that we want to animate, and then, start() is called to start the animation.

// React Native
class FadeInView extends React.Component {
  state = {
    fadeAnim: new Animated.Value(0) // Initial value for opacity: 0
  };
  componentDidMount() {
    Animated.timing(this.state.fadeAnim, {
      toValue: 1,
      duration: 10000
    }).start();
  }
  render() {
    return (
      <Animated.View style={{...this.props.style, opacity: this.state.fadeAnim }} >
        {this.props.children}
      </Animated.View>
    );
  }
}
    ...
<FadeInView>
  <Text> Fading in </Text>
</FadeInView>
    ...

要在 Flutter 中实现相同的动画效果,创建一个 AnimationController 对象,叫它 controller,并且指定时间间隔。在默认配置下, AnimationController 会在给定时间间隔线性的生成从 0.0 到 1.0 的数值。当你的程序可以显示新一帧画面的时候,AnimationController 会生成一个新的值。通常,这个频率在每秒 60 个值。

To create the same animation in Flutter, create an AnimationController object named controller and specify the duration. By default, an AnimationController linearly produces values that range from 0.0 to 1.0, during a given duration. The animation controller generates a new value whenever the device running your app is ready to display a new frame. Typically, this rate is around 60 values per second.

当定义 AnimationController 的时候,你必须传入一个 vsync 对象。 vsync 会防止屏幕显示区域之外的动画消耗不必要的资源。你可以通过添加 TickerProviderStateMixin 到类定义中来使用有状态的对象。 AnimationController 需要传入一个 TickerProvider,它是通过构造函数里的 vsync 参数进行配置的。

When defining an AnimationController, you must pass in a vsync object. The presence of vsync prevents offscreen animations from consuming unnecessary resources. You can use your stateful object as the vsync by adding TickerProviderStateMixin to the class definition. An AnimationController needs a TickerProvider, which is configured using the vsync argument on the constructor.

Tween 定义了起始和结束值之间或者输入段到输出段之间的过渡。如果要在动画中使用 Tween 对象,调用 Tween 对象的 animate 方法,然后把它赋给你要修改的 Animation 对象。

A Tween describes the interpolation between a beginning and ending value or the mapping from an input range to an output range. To use a Tween object with an animation, call the Tween object’s animate() method and pass it the Animation object that you want to modify.

在这个例子中,用到了 FadeTransition widget,它的 opacity 属性映射到了 animation 对象上。

For this example, a FadeTransition widget is used and the opacity property is mapped to the animation object.

要开始动画,使用 controller.forward()。其它的操作也可以使用控制器里的方法,比如 fling() 或者 repeat()。这个例子里,FlutterLogo widget 被用于 FadeTransition widget 中。

To start the animation, use controller.forward(). Other operations can also be performed using the controller such as fling() or repeat(). For this example, the FlutterLogo widget is used inside the FadeTransition widget.


// Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(Center(child: LogoFade()));
}

class LogoFade extends StatefulWidget {
  _LogoFadeState createState() => _LogoFadeState();
}

class _LogoFadeState extends State<LogoFade> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 3000), vsync: this);
    final CurvedAnimation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeIn);
    animation = Tween(begin: 0.0, end: 1.0).animate(curve);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: animation,
      child: Container(
        height: 300.0,
        width: 300.0,
        child: FlutterLogo(),
      ),
    );
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}
Flutter fade on Android
Android
Flutter fade on iOS
iOS

如何为卡片添加滑动动画呢?

How do I add swipe animation to cards?

在 React Native 中,无论 PanResponder 或者第三方库都可被用于滑动动画。

In React Native, either the PanResponder or third-party libraries are used for swipe animation.

在 Flutter 中,要添加滑动动画,使用 Dismissible widget 封装其它子 widget 即可。

In Flutter, to add a swipe animation, use the Dismissible widget and nest the child widgets.

child: Dismissible(
  key: key,
  onDismissed: (DismissDirection dir) {
    cards.removeLast();
  },
  child: Container(
    ...
  ),
),
Card swipe on Android
Android
Card swipe on iOS
iOS

React Native 和 Flutter widget 对等的组件

React Native and Flutter widget equivalent components

下面的表格列举了通用的 React Native 组件与对应的 Flutter widget 和通用的 widget 属性。

The following table lists commonly-used React Native components mapped to the corresponding Flutter widget and common widget properties.

React Native 组件 Flutter Widget 描述    
Button ElevatedButton 基础的凸起按钮    
  onPressed [required] 该回调函数在当按钮被点击的时候被触发。    
  Child 按钮的标签    

 

Button

 

TextButton

基础的扁平化按钮.

 

   
  onPressed [required] 当按钮被点击的时候触发该回调函数。    
  Child 按钮的标签    

 

ScrollView

 

ListView

一个可滑动的纵向排列的 widget 列表。

 

   
  children ( <Widget> [ ]) 要显示的子 widget 列表    
  controller [ ScrollController ] 可用于控制滑动 widget 的对象    
  itemExtent [ double ] 如果非空,那么强制所有子 widget 在滑动方向上增加给定的距离    
  scroll Direction [ Axis ] 滑动页面的滑动轴    

 

FlatList

 

ListView.builder

根据需要创建的一组 widget 的构造函数。

 

   
  itemBuilder [required] [IndexedWidgetBuilder] 根据需要创建子 widget。当元素序号大于等于零并且小于队列元素总数时,该回调函数会被调用。    
  itemCount [ int ] 优化了 ListView 对于最大滑动范围的预估能力。    

 

Image

 

Image

显示图片的 widget。

 

   
  image [required] 要显示的图片    
  Image. asset 有多个构造函数可以用于指定图片。    
  width, height, color, alignment 图片的风格和布局。    
  fit 将图片内嵌到布局对应的空间里。    

 

Modal

 

ModalRoute

避免和之前路径交叉的路径。

 

   
  animation 路径切换的动画和之前路径向前切换的动画。    

 

ActivityIndicator

 

LinearProgressIndicator

一个进度条 widget。

 

   
  strokeWidth 圆形线条的宽度。    
  backgroundColor 指示进度的背景色。默认是当前主题的 ThemeData.backgroundColor    

 

ActivityIndicator

 

LinearProgressIndicator

一个水平条形的进度条。

 

   
  value 进度条的进度值。    

 

RefreshControl

 

RefreshIndicator

支持 Material 中滑动刷新的 widget

 

   
  color 进度指示的前景色。    
  onRefresh 当用户拖拽刷新指示器想要刷新的时候会调用该函数。    

 

View

 

Container

封装子 widget 的 widget。

 

   

 

View

 

Column

将子 widget 纵向排列的 widget。

 

   

 

View

 

Row

将子 widget 横向排列的 widget。

 

   

 

View

 

Center

将子 widget 放置于中央的 widget。

 

   
View Padding 将子 widget 按照给定的间隔进行排列的 widget。    
  padding [required] [ EdgeInsets ] 子 widget 间隔。    

 

TouchableOpacity

 

GestureDetector

检测手势的 widget。

 

   
  onTap 当点击的时候会调用。    
  onDoubleTap 当两次点击的时候会调用。    

 

TextInput

 

TextInput

调用系统文本输入的接口。

 

   
  controller [ TextEditingController ] 用于获取或者修改文本。    

 

Text

 

Text

以单一的样式显示文本的文本 widget。

 

   
  data [ String ] 要显示的文本。    
  textDirection [ TextAlign ] 文本的方向。    

 

Switch

 

Switch

Material Design 样式的开关。

 

   
  value [required] [ boolean ] 开关的开启或者闭合状态。    
  onChanged [required] [ callback ] 当用户点击开关的时候调用。    

 

Slider

 

Slider

选择一个范围的值。

 

   
  value [required] [ double ] 当前滑动器的值。    
  onChanged [required] 当用户为滑动器选择了新的值时会调用    

给 Web 开发者的 Flutter 指南

目录

本文是为那些熟悉用 HTML 与 CSS 语法来管理应用页面中元素的开发者准备的。本文会将 HTML/CSS 代码片段替换为等价的 Flutter/Dart 代码。

This page is for users who are familiar with the HTML and CSS syntax for arranging components of an application’s UI. It maps HTML/CSS code snippets to their Flutter/Dart code equivalents.

One of the fundamental differences between designing a web layout and a Flutter layout, is learning how constraints work, and how widgets are sized and positioned. To learn more, see Understanding constraints.

这些示例包含如下假设:

The examples assume:

执行基础布局操作

Performing basic layout operations

以下示例将向你展示如何执行最常见的 UI 布局操作。

The following examples show how to perform the most common UI layout tasks.

文本样式与对齐

Styling and aligning text

CSS 所处理的字体样式、大小以及其他文本属性,都是一个 Text widget 子元素 TextStyle 中单独的属性。

Font style, size, and other text attributes that CSS handles with the font and color properties are individual properties of a TextStyle child of a Text widget.

Text widget 中的 textAlign 属性与 CSS 中的 text-align 属性作用相同,用来控制文本的对齐方向。

For text-align property in CSS that is used for aligning text, there is a textAlign property of a Text widget.

在 HTML 和 Flutter 中,子元素或者 widget 都默认锚定在左上方。

In both HTML and Flutter, child elements or widgets are anchored at the top left, by default.

<div class="greybox">
    Lorem ipsum
</div>

.greybox {
      background-color: #e0e0e0; /* grey 300 */
      width: 320px;
      height: 240px;
      font: 900 24px Georgia;
      text-align: center;
    }
var container = Container( // grey box
  child: Text(
    "Lorem ipsum",
    textAlign: TextAlign.center,
    style: TextStyle(
      fontSize: 24,
      fontWeight: FontWeight.w900,
      fontFamily: "Georgia",
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

设置背景颜色

设置背景颜色

Setting background color

在 Flutter 中,你可以通过 Containerdecoration 或者 color 属性来设置背景颜色。但是,你不能同时设置这两个属性,这有可能导致 decoration 覆盖掉 color。当背景是简单的颜色时,应首选 color 属性,对于其他情况,如渐变或图像,推荐使用 decoration 属性。

In Flutter, you set the background color using the color property or the decoration property of a Container. However, you cannot supply both, since it would potentially result in the decoration drawing over the background color. The color property should be preferred when the background is a simple color. For other cases, such as gradients or images, use the decoration property.

CSS 示例使用十六进制颜色,这等价于材质调色板。

The CSS examples use the hex color equivalents to the Material color palette.

<div class="greybox">
  Lorem ipsum
</div>

.greybox {
      background-color: #e0e0e0;  /* grey 300 */
      width: 320px;
      height: 240px;
      font: 900 24px Roboto;
    }
var container = Container( // grey box
  child: Text(
    "Lorem ipsum",
    style: bold24Roboto,
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);
var container = Container( // grey box
  child: Text(
    "Lorem ipsum",
    style: bold24Roboto,
  ),
  width: 320,
  height: 240,
  decoration: BoxDecoration(
    color: Colors.grey[300],
  ),
);

居中元素

Centering components

一个 Center widget 可以将它的子元素水平和垂直居中。

A Center widget centers its child both horizontally and vertically.

要用 CSS 实现相似的效果,父元素需要使用一个 flex 或者 table-cell 显示布局。本节示例使用的是 flex 布局。

To accomplish a similar effect in CSS, the parent element uses either a flex or table-cell display behavior. The examples on this page show the flex behavior.

<div class="greybox">
  Lorem ipsum
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center; 
}
var container = Container( // grey box
  child:  Center(
    child:  Text(
      "Lorem ipsum",
      style: bold24Roboto,
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

设置容器宽度

Setting container width

要指定一个 Container widget 的宽度,请使用它的 width 属性。和 CSS 中的 max-width 属性用于指定容器可调整的宽度最大值不同的是,这里指定的是一个固定宽度。要在 Flutter 中模拟该效果,可以使用 Container 的 constraints 属性。新建一个带有 minWidthmaxWidth 属性的 BoxConstraints widget。

To specify the width of a Container widget, use its width property. This is a fixed width, unlike the CSS max-width property that adjusts the container width up to a maximum value. To mimic that effect in Flutter, use the constraints property of the Container. Create a new BoxConstraints widget with a minWidth or maxWidth.

对嵌套的 Container 来说,如果其父元素宽度小于子元素宽度,则子元素会调整尺寸以匹配父元素大小。

For nested Containers, if the parent’s width is less than the child’s width, the child Container sizes itself to match the parent.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px; 
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  width: 100%;
  max-width: 240px; 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
      width: 240, //max-width is 240
    ),
  ),
  width: 320, 
  height: 240,
  color: Colors.grey[300],
);

操控位置与大小

Manipulating position and size

以下示例将展示如何对 widget 的位置、大小以及背景进行更复杂的操作。

The following examples show how to perform more complex operations on widget position, size, and background.

设置绝对位置

Setting absolute position

默认情况下, widget 相对于其父元素定位。

By default, widgets are positioned relative to their parent.

要通过 x-y 坐标指定一个 widget 的绝对位置,把它嵌套在一个 Positioned widget 中,而该 widget 则需被嵌套在一个 Stack widget 中。

To specify an absolute position for a widget as x-y coordinates, nest it in a Positioned widget that is, in turn, nested in a Stack widget.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  position: relative; 
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  position: absolute;
  top: 24px;
  left: 24px; 
}
var container = Container( // grey box
  child: Stack(
    children: [
      Positioned( // red box
        child:  Container(
          child: Text(
            "Lorem ipsum",
            style: bold24Roboto,
          ),
          decoration: BoxDecoration(
            color: Colors.red[400],
          ),
          padding: EdgeInsets.all(16),
        ),
        left: 24,
        top: 24,
      ),
    ],
  ), 
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

旋转元素

Rotating components

要旋转一个 widget,请将它嵌套在 Transform widget 中。使用 Transform widget 的 alignmentorigin 属性分别来指定转换原点(支点)的相对和绝对位置信息。

To rotate a widget, nest it in a Transform widget. Use the Transform widget’s alignment and origin properties to specify the transform origin (fulcrum) in relative and absolute terms, respectively.

对于简单的 2D 旋转,widget 是依据弧度在 Z 轴上旋转的,创建一个新的 Matrix4 标志对象,并使用它的 rotateZ() 方法使用弧度系数 (角度 × π / 180) 以指定旋转系数。

For a simple 2D rotation, in which the widget is rotated on the Z axis, create a new Matrix4 identity object and use its rotateZ() method to specify the rotation factor using radians (degrees × π / 180).

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  transform: rotate(15deg); 
}
var container = Container( // gray box
  child: Center(
    child:  Transform(
      child:  Container( // red box
        child: Text(
          "Lorem ipsum",
          style: bold24Roboto,
          textAlign: TextAlign.center,
        ),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        padding: EdgeInsets.all(16),
      ),
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..rotateZ(15 * 3.1415927 / 180),
    ), 
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

缩放元素

Scaling components

要缩放或放大一个 widget,请将它嵌套在一个 Transform widget 中。使用 Transform widget 的 alignmentorigin 属性分别来指定缩放原点(支点)的相对和绝对信息。

To scale a widget up or down, nest it in a Transform widget. Use the Transform widget’s alignment and origin properties to specify the transform origin (fulcrum) in relative or absolute terms, respectively.

对于沿 x 轴的简单缩放操作,新建一个 Matrix4 标识对象并用它的 scale() 方法来指定缩放因系数。

For a simple scaling operation along the x-axis, create a new Matrix4 identity object and use its scale() method to specify the scaling factor.

当你缩放一个父 widget 时,它的子 widget 也会相应被缩放。

When you scale a parent widget, its child widgets are scaled accordingly.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  transform: scale(1.5); 
}
var container = Container( // gray box
  child: Center(
    child:  Transform(
      child:  Container( // red box
        child: Text(
          "Lorem ipsum",
          style: bold24Roboto,
          textAlign: TextAlign.center,
        ),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        padding: EdgeInsets.all(16),
      ),
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..scale(1.5),
     ), 
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

应用线性变换

Applying a linear gradient

要将线性变换应用在 widget 的背景上,请将它嵌套在一个 Container widget 中。然后用 Container widget 的 decoration 属性生成一个 BoxDecoration 对象,然后使用 BoxDecoration 的 gradient 属性来变换背景填充内容。

To apply a linear gradient to a widget’s background, nest it in a Container widget. Then use the Container widget’s decoration property to create a BoxDecoration object, and use BoxDecoration’s gradient property to transform the background fill.

变换“角度”基于 Alignment (x, y) 取值来定:

The gradient “angle” is based on the Alignment (x, y) values:

垂直变换

Vertical gradient

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  padding: 16px;
  color: #ffffff;
  background: linear-gradient(180deg, #ef5350, rgba(0, 0, 0, 0) 80%); 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: const Alignment(0.0, -1.0),
          end: const Alignment(0.0, 0.6),
          colors: <Color>[
            const Color(0xffef5350),
            const Color(0x00ef5350)
          ],
        ),
      ), 
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

水平变换

Horizontal gradient

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  padding: 16px;
  color: #ffffff;
  background: linear-gradient(90deg, #ef5350, rgba(0, 0, 0, 0) 80%); 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: const Alignment(-1.0, 0.0),
          end: const Alignment(0.6, 0.0),
          colors: <Color>[
            const Color(0xffef5350),
            const Color(0x00ef5350)
          ],
        ),
      ), 
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

操控图形

Manipulating shapes

以下示例将展示如何新建和自定义图形。

The following examples show how to make and customize shapes.

圆角

Rounding corners

在矩形上实现圆角,请用 BoxDecoration 对象的 borderRadius 属性。新建一个 BorderRadius 对象来指定每个圆角的半径大小。

To round the corners of a rectangular shape, use the borderRadius property of a BoxDecoration object. Create a new BorderRadius object that specifies the radius for rounding each corner.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* gray 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  border-radius: 8px; 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red circle
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
        borderRadius: BorderRadius.all(
          const Radius.circular(8),
        ), 
      ),
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

添加盒阴影 (box shadows)

Adding box shadows

在 CSS 中你可以通过 box-shadow 属性快速指定阴影偏移与模糊范围。本例展示了两个盒阴影的属性设置:

In CSS you can specify shadow offset and blur in shorthand, using the box-shadow property. This example shows two box shadows, with properties:

在 Flutter 中,每个属性与其取值都是单独指定的。请使用 BoxDecoration 的 boxShadow 属性来生成一系列 BoxShadow widget。你可以定义一个或多个 BoxShadow widget,这些 widget 共同用于设置阴影深度、颜色等等。

In Flutter, each property and value is specified separately. Use the boxShadow property of BoxDecoration to create a list of BoxShadow widgets. You can define one or multiple BoxShadow widgets, which can be stacked to customize the shadow depth, color, and so on.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.8),
              0 6px 20px rgba(0, 0, 0, 0.5);
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
        boxShadow: [
          BoxShadow (
            color: const Color(0xcc000000),
            offset: Offset(0, 2),
            blurRadius: 4,
          ),
          BoxShadow (
            color: const Color(0x80000000),
            offset: Offset(0, 6),
            blurRadius: 20,
          ),
        ], 
      ),
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  decoration: BoxDecoration(
    color: Colors.grey[300],
  ),
  margin: EdgeInsets.only(bottom: 16),
);

生成圆与椭圆

Making circles and ellipses

尽管 CSS 中有 基础图形,用 CSS 生成圆可以用一个变通方案,即将矩形的四边 border-radius 均设成50%。

Making a circle in CSS requires a workaround of applying a border-radius of 50% to all four sides of a rectangle, though there are basic shapes.

虽然 BoxDecorationborderRadius 属性支持这样设置,Flutter 为 BoxShape enum 提供一个 shape 属性用于实现同样的目的。

While this approach is supported with the borderRadius property of BoxDecoration, Flutter provides a shape property with BoxShape enum for this purpose.

<div class="greybox">
  <div class="redcircle">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* gray 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redcircle {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  text-align: center;
  width: 160px;
  height: 160px;
  border-radius: 50%; 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red circle
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
        textAlign: TextAlign.center, 
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
        shape: BoxShape.circle, 
      ),
      padding: EdgeInsets.all(16),
      width: 160,
      height: 160, 
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

操控文本

Manipulating text

以下示例展示了如何设置字体和其他文本属性。他们同时还展示了如何变换文本字符、自定义间距以及生成摘要。

The following examples show how to specify fonts and other text attributes. They also show how to transform text strings, customize spacing, and create excerpts.

文字间距调整

Adjusting text spacing

在 CSS 中你可以通过分别给 letter-spacing 和 word-spacing 属性的长度赋值来指定每个字母��及每个单词间的空白距离。距离的单位可以是 px, pt, cm, em 等等。

In CSS you specify the amount of white space between each letter or word by giving a length value for the letter-spacing and word-spacing properties, respectively. The amount of space can be in px, pt, cm, em, etc.

在 Flutter 中,你可以在 Text widget 子元素 TextStyleletterSpacingwordSpacing 属性中将间距设置为逻辑像素(允许负值)。

In Flutter, you specify white space as logical pixels (negative values are allowed) for the letterSpacing and wordSpacing properties of a TextStyle child of a Text widget.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  letter-spacing: 4px; 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: TextStyle(
          color: Colors.white,
          fontSize: 24,
          fontWeight: FontWeight.w900,
          letterSpacing: 4, 
        ),
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

内联样式更改

Making inline formatting changes

一个 Text widget 允许你展示同一类样式的文本。为了展现具有多种样式(本例中,是一个带重音的单词)的文本,需要改用 RichText widget。它的 text 属性可以指定一个或多个可以单独设置样式的 TextSpan widget。

A Text widget lets you display text with some formatting characteristics. To display text that uses multiple styles (in this example, a single word with emphasis), use a RichText widget instead. Its text property can specify one or more TextSpan widgets that can be individually styled.

在接下来的示例中,”Lorem” 位于 TextSpan widget 中,具有默认(继承)文本样式,”ipsum” 位于具有自定义样式、单独的一个 TextSpan 中。

In the following example, “Lorem” is in a TextSpan widget with the default (inherited) text styling, and “ipsum” is in a separate TextSpan with custom styling.

<div class="greybox">
  <div class="redbox">
    Lorem <em>ipsum</em>
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
}
 .redbox em {
  font: 300 48px Roboto;
  font-style: italic;
} 
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child:  RichText(
        text: TextSpan(
          style: bold24Roboto,
          children: <TextSpan>[
            TextSpan(text: "Lorem "),
            TextSpan(
              text: "ipsum",
              style: TextStyle(
                fontWeight: FontWeight.w300,
                fontStyle: FontStyle.italic,
                fontSize: 48,
              ),
            ),
          ],
        ),
      ), 
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

生成文本摘要

Creating text excerpts

一个摘要会展示一个段落中文本的初始行内容,并常用省略号处理溢出的文本内容。在 HTML/CSS 中,摘录不能超过一行。在多行之后进行截断需要运行一些 JavaScript 代码。

An excerpt displays the initial line(s) of text in a paragraph, and handles the overflow text, often using an ellipsis. In HTML/CSS an excerpt can be no longer than one line. Truncating after multiple lines requires some JavaScript code.

在 Flutter 中,使用 Text widget 的 maxLines 属性来指定包含在摘要中的行数,以及 overflow 属性来处理溢出文本。

In Flutter, use the maxLines property of a Text widget to specify the number of lines to include in the excerpt, and the overflow property for handling overflow text.

<div class="greybox">
  <div class="redbox">
    Lorem ipsum dolor sit amet, consec etur
  </div>
</div>

.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; 
}
var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum dolor sit amet, consec etur",
        style: bold24Roboto,
        overflow: TextOverflow.ellipsis,
        maxLines: 1, 
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
    ),
  ),
  width: 320,
  height: 240,
  color: Colors.grey[300],
);

给 Xamarin.Forms 开发者的 Flutter 指南

目录

本文档旨在帮助 Xamarin.Forms 开发者利用已有的知识去构建 Flutter 移动应用。如果你懂得 Xamarin.Forms 框架的基本原理,那么你就可以将本文档当作你开始 Flutter 开发的不错的起点。

This document is meant for Xamarin.Forms developers looking to apply their existing knowledge to build mobile apps with Flutter. If you understand the fundamentals of the Xamarin.Forms framework, then you can use this document as a jump start to Flutter development.

你的 Android 和 iOS 知识以及技能组合在构建 Flutter 时都是有价值的,因为 Flutter 依赖的原生系统配置都与你配置 Xamarin.Forms 原生项目时一样。 Flutter 框架与你创建一个单独的界面时也是一样的,这在多个平台中同样适用。

Your Android and iOS knowledge and skill set are valuable when building with Flutter, because Flutter relies on the native operating system configurations, similar to how you would configure your native Xamarin.Forms projects. The Flutter Frameworks is also similar to how you create a single UI, that is used on multiple platforms.

本文档可用做可指导手册来翻查与你需求最为相关的问题。

This document can be used as a cookbook by that are most relevant to your needs.

项目设置

Project setup

app 是如何运行的?

How does the app start?

对于 Xamarin.Forms 里的每个平台,你可以调用 LoadApplication 方法,创建一个新应用并运行你的 app 。

For each platform in Xamarin.Forms, which creates a new application and starts your app.

LoadApplication(new App());

在 Flutter 中,加载 Flutter app 的默认主入口点是 main

In Flutter, the default main entry point is main where you load your Flutter app.

void main() {
  runApp(MyApp());
}

在 Xamarin.Forms 中,你分配一个 PageApplication 类中的 MainPage 属性。

In Xamarin.Forms, you assign a Page to the MainPage property in the Application class.

public class App : Application
{
    public App()
    {
        MainPage = new ContentPage
        {
            Content = new Label
            {
                Text = "Hello World",
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center
            }
        };
    }
}

在 Flutter 中,“万物皆 widget”,甚至连应用本身也是。接下来的示例展示了 MyApp ,一个简单的应用 Widget

In Flutter, “everything is a widget”, even the application itself. The following example shows MyApp, a simple application Widget.

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Text("Hello World!", textDirection: TextDirection.ltr));
  }
}

如何创建一个页面?

How do you create a page?

Xamarin.Forms 拥有一些不同类型的页面; ContentPage 是最为通用的。在 Flutter 中,指定一个应用程序 widget 来控制你的根页面。你可以使用一个 MaterialApp widget,它支持 Material Design,或者你也可以使用 CupertinoApp widget,它能用来创建 ios 风格的应用,或者你也可以使用等级较低的 WidgetsApp,可供你随心所欲地定制。

Xamarin.Forms has many different types of pages; ContentPage is the most common. In Flutter, you specify an application widget that holds your root page. You can use a MaterialApp widget, which supports Material Design, or you can use a CupertinoApp widget, which supports an iOS-style app, or you can use the lower level WidgetsApp, which you can customize in any way you want.

接下来的代码定义了一个主页,一个有状态的 widget。在 Flutter 中,除了以下两个类型的 widget 外,其它 widget 都是不可变的:有状态和无状态 widget。无状态 widget 的示例都是标题、图标或图片。

The following code defines the home page, a stateful widget. In Flutter, all widgets are immutable, but two types of widgets are supported: stateful and stateless. Examples of a stateless widget are titles, icons, or images.

下面的示例使用 MaterialApp,它在 home 属性中控制它的根页面。

The following example uses MaterialApp, which holds its root page in the home property.

class MyApp extends StatelessWidget {
  // This widget is the root of your application(这个 widget 是你的应用程序的根 widget)。

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

从这里开始,真正的首页是另一个你在里面创建了状态的 widget

From here, your actual first page is another Widget, in which you create your state.

一个有状态 widget,例如下面的 MyHomePage,包含两个部分。第一部分,是它自身不变的,创建一个状态对象(State object)来管控对象的状态。状态对象持续存在于 widget 的整个生命周期中。

A stateful widget, such as MyHomePage below, consists of two parts. The first part, which is itself immutable, creates a State object that holds the state of the object. The State object persists over the life of the widget.

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

状态对象实现了有状态 widget 中的构建方法。

The State object implements the build() method for the stateful widget.

当 widget 树的状态发生了改变,将会调用 setState() 触发 widget 当中该部分UI的构建。确保只在需要时调用 setState() ,并且在只有部分 widget 树发生变化时调用,否则会造成糟糕的UI性能表现。

When the state of the widget tree changes, call setState(), which triggers a build of that portion of the UI. Make sure to call setState() only when necessary, and only on the part of the widget tree that has changed, or it can result in poor UI performance.

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set the appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

在 Flutter 中的UI(也就是这里所说的 widget 树)是不可变的,意思是说它一旦被构建,你就无法再改变他的状态。当你修改状态 类中的字段,就要再次调用 setState 来重新构建整个 widget 树。

In Flutter, the UI (also known as widget tree), is immutable, meaning you can’t change its state once it’s built. You change fields in your State class, then call setState() to rebuild the entire widget tree again.

这个生成UI的方式不同于 Xamarin.Forms,但是这种方法却有很多益处。

This way of generating UI is different than Xamarin.Forms, but there are many benefits to this approach.

视图

Views

在 Flutter 中页面(Page)与元素(Element)的相同的是什么?

What is the equivalent of a Page or Element in Flutter?

一个 ContentPageTabbedPageFlyoutPage 就是你可以在 Xamarin.Forms 应用程序中使用的全部页面类型。这些页面会控制元素(Element)来显示各种控件。在 Xamarin.Forms 中,Entry 或者 Button 就是一个 元素 的示例。

ContentPage, TabbedPage, FlyoutPage are all types of pages you might in a Xamarin.Forms application. These pages would then hold Elements to display the various controls. In Xamarin.Forms an Entry or Button are examples of an Element.

在 Flutter 中,几乎所有东西都是 widget,一个页面在 Flutter 中被称作路由(Route),也是一个 widget。按钮、进度条、动画控制器都是 widget。当构建一个路由时,就会创建一棵 widget 树。

In Flutter, almost everything is a widget. A Page, called a Route in Flutter, is a widget. Buttons, progress bars, and animation controllers are all widgets. When building a route, you create a widget tree.

Flutter 包含 Material 组件 库。这些都是实现了 Material Design 指南 的 widget。 Material Design 是一个灵活的 针对所有平台 的设计系统,包括 iOS。

Flutter includes the Material Components library. These are widgets that implement the Material Design guidelines. Material Design is a flexible design system optimized for all platforms, including iOS.

不过,Flutter 有足够灵活和自描述性 (expressive) 去实现任何设计语言。举个例子,在 iOS 上,你可以用 Cupertino widgets 来生成一个看起来像 苹果 iOS 设计语言 的接口。

But Flutter is flexible and expressive enough to implement any design language. For example, on iOS, you can use the Cupertino widgets to produce an interface that looks like Apple’s iOS design language.

如何更新 widget?

How do I update widgets?

在 Xamarin.Forms 中,每一个页面或者元素都是一个有状态的类,拥有一些属性和方法。通过更新一个属性来更新你的元素,而且这会传递到原生控件。

In Xamarin.Forms, each Page or Element is a stateful class, that has properties and methods. You update your Element by updating a property, and this is propagated down to the native control.

在 Flutter 中,widget是不可变的,你不可以直接地通过修改一个属性来更新它们,而是应该使用 widget 的状态。

In Flutter, Widgets are immutable and you can’t directly update them by changing a property, instead you have to work with the widget’s state.

有状态 widget 和无状态 widget 的概念就是出自这里, 无状态 widget(StatelessWidget)顾名思义,就是一个没有状态信息的 widget。

This is where the concept of Stateful vs Stateless widgets comes from. A StatelessWidget is just what it sounds like—a widget with no state information.

当你在描绘用户界面的一个不依赖除对象中的配置信息之外任何东西的部分时, StatelessWidgets 是有用的。

StatelessWidgets are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object.

举个例子,在 Xamarin.Forms 中,可以轻而易举地用你的logo替换一张图片。这个logo将不会在运行过程中修改,所以在 Flutter 会使用StatelessWidget

For example, in Xamarin.Forms, this is similar to placing an Image with your logo. The logo is not going to change during runtime, so use a StatelessWidget in Flutter.

如果你想动态地基于进行了 HTTP 调用或者用户交互后接收到的数据来修改 UI,你需要使用 StatefulWidget 并告诉 Flutter 框架这个 widget 的 状态(State) 已经被更新了所以它可以更新那个 widget。

If you want to dynamically change the UI based on data received after making an HTTP call or user interaction then you have to work with StatefulWidget and tell the Flutter framework that the widget’s State has been updated so it can update that widget.

这里要记下的重要内容是有状态和无状态 widget 的核心行为都是一样的。他们重建每个结构,不同的是StatefulWidget拥有一个状态(State) 对象来跨结构储存状态数据和恢复它。

The important thing to note here is at the core both stateless and stateful widgets behave the same. They rebuild every frame, the difference is the StatefulWidget has a State object that stores state data across frames and restores it.

如果你有疑惑,那么就记住这个规则:如果一个 widget 改变了(例如是因为用户交互),它就是有状态的。相反,如果一个 widget 对修改作出反应,包含它的父 widget 如果本身没有对修改作出反应,仍然可以是无状态的。

If you are in doubt, then always remember this rule: if a widget changes (because of user interactions, for example) it’s stateful. However, if a widget reacts to change, the containing parent widget can still be stateless if it doesn’t itself react to change.

接下来的示例展示了如何使用一个StatelessWidget。一个公共的 StatelessWidgetText widget。如果你查阅 Text widget 的实现,你会发现他是 StatelessWidget 的子类。

The following example shows how to use a StatelessWidget. A common StatelessWidget is the Text widget. If you look at the implementation of the Text widget you’ll find it subclasses StatelessWidget.

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如你所见,文本 widget 没有状态信息与它关联,它只渲染在它的构造函数中呈现的内容,没有更多。

As you can see, the Text widget has no state information associated with it, it renders what is passed in its constructors and nothing more.

但是,如果你想动态地作出 “I Like Flutter”的修改呢?例如在点击一个FloatingActionButton时。

But, what if you want to make “I Like Flutter” change dynamically, for example when clicking a FloatingActionButton?

为了实现这个目标,需要将 Text widget 封装到一个StatefulWidget中,并在用用户点击按钮时更新它,正如接下来的例子:

To achieve this, wrap the Text widget in a StatefulWidget and update it when the user clicks the button, as shown in the following example:

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  void _updateText() {
    setState(() {
      // Update the text
      textToShow = "Flutter is Awesome!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

该如何布局我的 widget 呢?什么东西可以等价于一个 XAML 文件?

How do I lay out my widgets? What is the equivalent of an XAML file?

在 Xamarin.Forms 中,大部分开发者用 XAML 写布局,尽管有时用 C#。在 Flutter 中编码一棵 widget 树来编写布局。

In Xamarin.Forms, most developers write layouts in XAML, though sometimes in C#. In Flutter, you write your layouts with a widget tree in code.

接下来的示例展示如何显示一个简单的带填充(padding)的 widget:

The following example shows how to display a simple widget with padding:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          padding: EdgeInsets.only(left: 20.0, right: 30.0),
        ),
        onPressed: () {},
        child: Text('Hello'),
      ),
    ),
  );
}

您可以查看 Flutter 在 widget 目录 中提供的布局。

You can view the layouts that Flutter has to offer in the widget catalog.

如何从布局中添加或移除一个元素?

How do I add or remove an Element from my layout?

在 Xamarin.Forms 中,你需要在代码中移除或添加一个 元素(Element)。如果它说一个列表,这将会涉及设置 Content 属性或者调用 Add() 或者 Remove() 方法。

In Xamarin.Forms, you had to remove or add an Element in code. This involved either setting the Content property or calling Add() or Remove() if it was a list.

在 Flutter 中,因为 widget 都是不可变的,所以没有直接对等的东西。相反,你可以将一个返回一个 widget 的函数传递给父级,并用布尔标控制它的子 widget 的创建。

In Flutter, because widgets are immutable there is no direct equivalent. Instead, you can pass a function to the parent that returns a widget, and control that child’s creation with a boolean flag.

下面的示例展示当用户点击 FloatingActionButton 时,如何在两个 widget 之间切换。

The following example shows how to toggle between two widgets when the user clicks the FloatingActionButton:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

如何让一个 widget 动起来?

How do I animate a widget?

在 Xamarin.Forms 中,你可以利用包括例如 FadeToTranslateTo 等方法的视图扩展(ViewExtensions)来创建简单的动画。你会在一个视图中使用这些方法来执行需要的动画。

In Xamarin.Forms, you create simple animations using ViewExtensions that include methods such as FadeTo and TranslateTo. You would use these methods on a view to perform the required animations.

<Image Source="{Binding MyImage}" x:Name="myImage" />

然后再后面的代码或一个动作中,这个会在1秒内淡入这张图像。

Then in code behind, or a behavior, this would fade in the image, over a 1 second period.

myImage.FadeTo(0, 1000);

在 Flutter 中,通过封装 widget 到一个动画 widget 中,可以使用动画类库来让 widget 动起来。使用一个 AnimationController ,即一个可以暂停、寻找、停止和倒退动画的 Animation<double> 。它需要一个滴答器(Ticker),当垂直同步(vsync)发生时,会发出信号,并在运行时的每一帧都会产生0和1之间的线性插值。然后你可以创建一个或多个动画并把它们附加到控制器上。

In Flutter, you animate widgets using the animation library by wrapping widgets inside an animated widget. Use an AnimationController, which is an Animation<double> that can pause, seek, stop and reverse the animation. It requires a Ticker that signals when vsync happens, and produces a linear interpolation between 0 and 1 on each frame while it’s running. You then create one or more Animations and attach them to the controller.

举个例子,你可以使用 CurvedAnimation 来实现一个沿着插值曲线的动画。在这个场景中,控制器说一个动画进展的“大师”源,而 CurvedAnimation 计算用来替代控制器默认线性运动的曲线。跟 widget 一样,Flutter 中的动画与组成一起工作。

For example, you might use CurvedAnimation to implement an animation along an interpolated curve. In this sense, the controller is the “master” source of the animation progress and the CurvedAnimation computes the curve that replaces the controller’s default linear motion. Like widgets, animations in Flutter work with composition.

当你在构建一个 widget 树,赋值一个 动画(Animation) 给一个 widget 的一个动画属性时,比如 渐退(FadeTransition) 的不透明度,会告诉控制器开始执行动画。

When building the widget tree, you assign the Animation to an animated property of a widget, such as the opacity of a FadeTransition, and tell the controller to start the animation.

下面的实例展示如何去写一个 渐退(FadeTransition),当你按下 FloatingActionButton 时,它会把 widget 渐变到一个logo。

The following example shows how to write a FadeTransition that fades the widget into a logo when you press the FloatingActionButton:

import 'package:flutter/material.dart';

void main() {
  runApp(FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: Container(
              child: FadeTransition(
                  opacity: curve,
                  child: FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}

更多信息,可以查阅 动画 & 运动 widget动画教程,以及动画概述

For more information, see Animation & Motion widgets, the Animations tutorial, and the Animations overview.

如何在屏幕上绘图?

How do I draw/paint on the screen?

Xamarin.Forms 从来没有任何内置的方法来直接在屏幕上绘图。如果他们需要一个自定义图像绘制,大多数使用 SkiaSharp。在 Flutter中,你可以直接访问 Skia 画布(Skia Canvas)方便地在屏幕上绘图。

Xamarin.Forms never had a built in way to draw directly on the screen. Many would use SkiaSharp, if they needed a custom image drawn. In Flutter, you have direct access to the Skia Canvas and can easily draw on screen.

Flutter 拥有两个类来帮助你在画布上绘图:CustomPaintCustomPainter,后者实现了你在画布上绘图的算法。

Flutter has two classes that help you draw to the canvas: CustomPaint and CustomPainter, the latter of which implements your algorithm to draw to the canvas.

如果想学习如何在 Flutter 中实现一个签名画手,请阅读 Collin 在 StackOverflow 的回答。

To learn how to implement a signature painter in Flutter, see Collin’s answer on StackOverflow.

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

widget 的不透明度在哪里?

Where is the widget’s opacity?

Xamarin.Forms 上,所有 虚拟元素(VisualElement)都拥有一个不透明度。在 Flutter 中,你需要封装一个 widget 到一个 不透明度 widget 来实现它。

On Xamarin.Forms, all VisualElements have an Opacity. In Flutter, you need to wrap a widget in an Opacity widget to accomplish this.

如何构建一个自定义 widget ?

How do I build custom widgets?

在 Xamarin.Forms 中,通常派生 VisualElement 或使用一个已有的 VisualElement ,来重写和实现所需行为的方法。

In Xamarin.Forms, you typically subclass VisualElement, or use a pre-existing VisualElement, to override and implement methods that achieve the desired behavior.

在 Flutter 中,通过 组合(composing) 更小的 widget(而不是扩展它们)来构建一个自定义 widget。这有点类似于基于 Grid 实现自定义控件,其中添加了大量 VisualElement,同时使用自定义逻辑进行扩展。

In Flutter, build a custom widget by composing smaller widgets (instead of extending them). It is somewhat similar to implementing a custom control based off a Grid with numerous VisualElements added in, while extending with custom logic.

举个例子,如何构建一个在构造器接受一个标签的自定义按钮?创建一个组合了一个带有标签的RaisedButton的自定义按钮,而不是扩展 RaisedButton

For example, how do you build a CustomButton that takes a label in the constructor? Create a CustomButton that composes a ElevatedButton with a label, rather than by extending ElevatedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: () {}, child: Text(label));
  }
}

然后就可以像使用其他 Flutter widget 一样使用这个自定义按钮

Then use CustomButton, just as you’d use any other Flutter widget:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Navigation

如何在页面之间导航?

How do I navigate between pages?

在 Xamarin.Forms 中,NavigationPage 类提供了一个阶级式的导航方式,让用户可以在页面之间来回进行跳转。

In Xamarin.Forms, the NavigationPage class provides a hierarchical navigation experience where the user is able to navigate through pages, forwards and backwards.

Flutter 也有类似的实现,使用了一个导航器(Navigator)路由(Routes)。一个路由是一个应用程序里一个页面的抽象,而一个导航器是一个管理路由的 widget

Flutter has a similar implementation, using a Navigator and Routes. A Route is an abstraction for a Page of an app, and a Navigator is a widget that manages routes.

一个路由大致上映射到一个页面。导航器以类似 Xamarin.Forms NavigationPage 的方式工作,在里面可以 push()pop() 路由,依赖于你是否想导航到一个视图,或者从它返回。

A route dd maps to a Page. The navigator works in a similar way to the Xamarin.Forms NavigationPage, in that it can push() and pop() routes depending on whether you want to navigate to, or back from, a view.

在页面间导航,你有几个选择:

To navigate between pages, you have a couple options:

接下来构建一个映射的示例。

The following example builds a Map.

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

通过推入一个路由的名称到导航器来导航到这个路由。

Navigate to a route by pushing its name to the Navigator.

Navigator.of(context).pushNamed('/b');

导航器是一个管理你的应用程序的路由的堆栈。把一个路由推入堆栈可以移动到这个路由,而从堆栈弹出一个路由可以返回到前一个路由。这是通过等待push() 返回的 未来(Future) 来完成的。

The Navigator is a stack that manages your app’s routes. Pushing a route to the stack moves to that route. Popping a route from the stack, returns to the previous route. This is done by awaiting on the Future returned by push().

Async/await 与 .NET 的实现非常类似,并且是在 Async UI中有更详尽的解释。

Async/await is very similar to the .NET implementation and is explained in more detail in Async UI.

举个例子,要开始一个让用户选择他们的定位的 定位(location) 路由,你需要做以下步骤:

For example, to start a location route that lets the user select their location, you might do the following:

Map coordinates = await Navigator.of(context).pushNamed('/location');

然后,在你的“定位”路由里,一旦用户选择他们的定位,使用结果来 pop() 这个堆栈。

And then, inside your ‘location’ route, once the user has selected their location, pop the stack with the result:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

如何导航到其它应用程序?

How do I navigate to another app?

在 Xamarin.Forms 中,需要用指定的 URI 协议并使用 Device.OpenUrl("mailto://") 来传送用户到其它应用程序。

In Xamarin.Forms, to send the user to another application, you use a specific URI scheme, using Device.OpenUrl("mailto://")

为了在 Flutter 中实现这个功能,需要创建一个原生平台集成,或者使用 已有的插件,比如 url_launcher,可与在 [pub.dev 上的许多其他包一起使用。

To implement this functionality in Flutter, create a native platform integration, or use an existing plugin, such as url_launcher, available with many other packages on pub.dev.

异步 UI

Async UI

在 Flutter 中有什么是跟 Device.BeginOnMainThread() 方法是相等的?

What is the equivalent of Device.BeginOnMainThread() in Flutter?

Dart 拥有一个单线程执行模型,支持“隔离” (在另一个线程上运行Dart代码的方法)、事件循环和异步编程。除非生成一个“隔离”,否则您的Dart代码会在主UI线程中运行,并由一个事件循环来驱动。

Dart has a single-threaded execution model, with support for Isolates (a way to run Dart code on another thread), an event loop, and asynchronous programming. Unless you spawn an Isolate, your Dart code runs in the main UI thread and is driven by an event loop.

Dart 的单线程模型并不意味着需要以会导致UI冻结的阻塞操作方式来运行所有内容。更多地像 Xamarin.Forms 一样需要让 UI 线程保持空闲。您将使用“async”/“wait”来执行任务,其中必须等待响应。

Dart’s single-threaded model doesn’t mean you need to run everything as a blocking operation that causes the UI to freeze. Much like Xamarin.Forms, you need to keep the UI thread free. You would use async/await to perform tasks, where you must wait for the response.

在 Flutter 中,使用 Dart 语言提供的异步工具(也称为 async/await)来执行异步工作。这跟 C# 很像,并且对于 Xamarin.Forms 开发者来说应该是非常容易使用的。

In Flutter, use the asynchronous facilities that the Dart language provides, also named async/await, to perform asynchronous work. This is very similar to C# and should be very easy to use for any Xamarin.Forms developer.

例如,您可以使用 async/await 运行网络请求代码,而不会导致UI挂起,并让Dart完成繁重的工作:

For example, you can run network code without causing the UI to hang by using async/await and letting Dart do the heavy lifting:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

一旦完成等待的网络调用后,通过调用 setState() 更新UI,这将触发 widget 子树的重新构建并更新数据。

Once the awaited network call is done, update the UI by calling setState(), which triggers a rebuild of the widget sub-tree and updates the data.

下面的实例异步加载数据并在一个 ListView 中显示:

The following example loads data asynchronously and displays it in a ListView:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

有关在后台工作、以及 Flutter 与 Android 的不同之处的更多信息,请参考下一节。

Refer to the next section for more information on doing work in the background, and how Flutter differs from Android.

如何将工作转移到后台线程?

How do you move work to a background thread?

因为 Flutter 是单线程的,并且运行一个事件循环,所以您不必担心线程管理或产生后台线程。这一点与 Xamarin.Forms 非常相似。如果您正在做 I/O 密集型的工作,比如磁盘访问或网络调用,那么您可以安全地使用 async/await,这样就一切就绪了。

Since Flutter is single threaded and runs an event loop, you don’t have to worry about thread management or spawning background threads. This is very similar to Xamarin.Forms. If you’re doing I/O-bound work, such as disk access or a network call, then you can safely use async/await and you’re all set.

另一方面,如果您需要做计算密集型的工作,使CPU保持忙碌,那么您希望将它移动到“隔离”状态,以避免阻塞事件循环,就像您将任何类型的工作放在主线程之外一样。这类似于通过 Xamarin.Forms 中的 Task.Run() 将内容移动到另一个线程。

If, on the other hand, you need to do computationally intensive work that keeps the CPU busy, you want to move it to an Isolate to avoid blocking the event loop, like you would keep any sort of work out of the main thread. This is similar to when you move things to a different thread via Task.Run() in Xamarin.Forms.

对于 I/O 密集型的工作,将函数声明为一个 异步 函数,并在函数内部 等待 长时间运行的任务:

For I/O-bound work, declare the function as an async function, and await on long-running tasks inside the function:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

这是您通常执行网络或数据库调用的方式,它们都是I/O操作。

This is how you would typically do network or database calls, which are both I/O operations.

然而,有时您可能正在处理大量数据而UI挂起了。在 Flutter 中,使用隔离来利用多个CPU内核来执行长时间运行或计算密集型任务。

However, there are times when you might be processing a large amount of data and your UI hangs. In Flutter, use Isolates to take advantage of multiple CPU cores to do long-running or computationally intensive tasks.

隔离线程是独立的执行线程,不与主执行内存堆共享任何内存。这是与 Task.Run() 的区别。这意味着您不能从主线程访问变量,也不能通过调用 setState() 更新UI。

Isolates are separate execution threads that do not share any memory with the main execution memory heap. This is a difference between Task.Run(). This means you can’t access variables from the main thread, or update your UI by calling setState().

下面的示例以简单的方式展示了如何将数据共享回主线程以更新UI。

The following example shows, in a simple isolate, how to share data back to the main thread to update the UI.

loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message.
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(
    sendPort,
    "https://jsonplaceholder.typicode.com/posts",
  );

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate.
static dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

在这里,dataLoader() 是在它自己单独的执行线程中运行的隔离。在隔离中,您可以执行更多的CPU密集型处理(例如,解析大型JSON),或者执行计算密集型数学,如加密或信号处理。

Here, dataLoader() is the Isolate that runs in its own separate execution thread. In the isolate you can perform more CPU intensive processing (parsing a big JSON, for example), or perform computationally intensive math, such as encryption or signal processing.

你可以运行下面这个完整的例子:

You can run the full example below:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message.
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(
      sendPort,
      "https://jsonplaceholder.typicode.com/posts",
    );

    setState(() {
      widgets = msg;
    });
  }

  // the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

如何发送一个网络请求?

How do I make network requests?

在 Xamarin.Forms 中,你可以使用 HttpClient。当您使用流行的 http package 包时,在 Flutter 中进行网络调用就很容易了。这将抽象出许多您通常可能自己实现的网络,从而使网络调用变简化。

In Xamarin.Forms you would use HttpClient. Making a network call in Flutter is easy when you use the popular http package. This abstracts away a lot of the networking that you might normally implement yourself, making it simple to make network calls.

要使用 http 包,请将它添加到 pubspec.yaml 文件中的依赖项中:

To use the http package, add it to your dependencies in pubspec.yaml:

dependencies:
  ...
  http: ^0.11.3+16

To make a network request, call await on the async function http.get():

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

如何显示长时间运行的任务的进度?

How do I show the progress for a long-running task?

在 Xamarin.Forms 中常会创建一个加载指示器,可以直接在XAML中创建,也可以通过第三方插件创建,比如 AcrDialogs。

In Xamarin.Forms you would typically create a loading indicator, either directly in XAML or through a 3rd party plugin such as AcrDialogs.

在 Flutter 中,使用一个 加载指示器( ProgressIndicator)widget。通过一个布尔标志控制何时渲染来以编程方式显示进度。告诉 Flutter 在长时间运行的任务开始之前更新它的状态,并在任务结束后隐藏它。

In Flutter, use a ProgressIndicator widget. Show the progress programmatically by controlling when it’s rendered through a boolean flag. Tell Flutter to update its state before your long-running task starts, and hide it after it ends.

在下面的示例中,build 函数被分成三个不同的函数。如果 showLoadingDialog()true (即当widgets.length == 0时)就会渲染出 进度指示器。另一方面,用网络调用返回的数据渲染 列表视图(ListView)

In the following example, the build function is separated into three different functions. If showLoadingDialog() is true (when widgets.length == 0), then render the ProgressIndicator. Otherwise, render the ListView with the data returned from a network call.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}"),
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

项目结构与资源

Project structure & resources

如何储存我的图片文件?

Where do I store my image files?

Xamarin.Forms 没有独立于平台的存储图像的方法,您必须放置图片在 iOS 的 xcasset 文件夹, 或 Android 的 drawable 文件夹中。

Xamarin.Forms has no platform independent way of storing images, you had to place images in the iOS xcasset folder or on Android, in the various drawable folders.

Android和iOS将资源(resources)和资产(assets)视为不同的项目,而 Flutter 应用程序只有资产(assets)。 Resources/drawable-* 文件夹中的所有资源都放在一个 Flutter 的资产 (assets) 文件夹中。

While Android and iOS treat resources and assets as distinct items, Flutter apps have only assets. All resources that would live in the Resources/drawable-* folders on Android, are placed in an assets folder for Flutter.

Flutter 遵循一种与 iOS 类似的简单的基于密度(density-based)的格式。资产可能是 1.0x2.0x3.0x 或任何其他倍数。Flutter 没有 dp,但是有逻辑像素,这基本上是与设备无关像素相同。用所谓 devicePixelRatio 表示单个逻辑像素中物理像素的比例。

Flutter follows a simple density-based format like iOS. Assets might be 1.0x, 2.0x, 3.0x, or any other multiplier. Flutter doesn’t have dps but there are logical pixels, which are basically the same as device-independent pixels. The so-called devicePixelRatio expresses the ratio of physical pixels in a single logical pixel.

与 Android 的密度桶相等的是:

The equivalent to Android’s density buckets are:

Android density qualifier Flutter pixel ratio
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

资产位于任意文件夹中— Flutter 没有预定义的文件夹结构。在 pubspec.yaml 文件中声明资产(带有位置),Flutter 就会得到它们。

Assets are located in any arbitrary folder—Flutter has no predefined folder structure. You declare the assets (with location) in the pubspec.yaml file, and Flutter picks them up.

注意,在 Flutter 1.0 beta 2 之前的版本中,Flutter 中定义的资产并不能从原生一侧访问,反之亦然,原生资产和资源对 Flutter 无效,就像他们被放在单独的文件夹中。

Note that before Flutter 1.0 beta 2, assets defined in Flutter were not accessible from the native side, and vice versa, native assets and resources weren’t available to Flutter, as they lived in separate folders.

在 Flutter beta 2 版本中,资产都被存储在原生的资产文件夹中,并且可以通过 Android 的资产管理器(AssetManager) 从原生一侧被访问。

As of Flutter beta 2, assets are stored in the native asset folder, and are accessed on the native side using Android’s AssetManager:

在 Flutter beta 2 版本中,Flutter 仍然不能访问原生资源,也不能访问原生资产。

As of Flutter beta 2, Flutter still cannot access native resources, nor it can access native assets.

例如,如果要新建一个新的名为 my_icon.png 的图像资产到我们的 Flutter 项目,并决定它应该放在一个被我们随意命名为 images 的文件夹中,你需要把基础图像(1.0x)放到 images 文件夹中,而所有的其他变量的文件放在以与之对应的比率乘数命名的子文件夹中:

To add a new image asset called my_icon.png to our Flutter project, for example, and deciding that it should live in a folder we arbitrarily called images, you would put the base image (1.0x) in the images folder, and all the other variants in sub-folders called with the appropriate ratio multiplier:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下来,您需要在您的 pubspec.yaml 文件中声明这些图像:

Next, you’ll need to declare these images in your pubspec.yaml file:

assets:
 - images/my_icon.jpeg

之后就可以用 AssetImage 来访问你的图像了:

You can then access your images using AssetImage:

return AssetImage("images/a_dot_burr.jpeg");

或者可以直接在一个 Image widget 中访问:

or directly in an Image widget:

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

更多详尽的信息可以在 在 Flutter 中添加资产和图像 中找到。

More detailed information can be found in Adding assets and images.

在哪里存储字符串?如何处理本地化?

Where do I store strings? How do I handle localization?

与 .NET 拥有 resx 文件不同,Flutter 目前没有一个专门的字符串类资源系统。此时,最佳实践是将复制文本作为静态字段保存在类中,并从那里访问它们。举个例子:

Unlike .NET which has resx files, Flutter currently doesn’t have a dedicated resources-like system for strings. At the moment, the best practice is to hold your copy text in a class as static fields and accessing them from there. For example:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

那么在你的代码中,你可以像这样访问你的字符串:

Then in your code, you can access your strings as such:

Text(Strings.welcomeMessage)

默认情况下,Flutter 的字符串只支持美式英语。如果你需要添加其他语音的支持,可以包含 flutter_localizations 包。你可能还需要添加 Dart的 intl 包来使用 i10n 装置,例如日期、时间的格式化。

By default, Flutter only supports US English for its strings. If you need to add support for other languages, include the flutter_localizations package. You might also need to add Dart’s intl package to use i10n machinery, such as date/time formatting.

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

使用 flutter_localizations 包时,要在应用程序的 widget 上指定 localizationsDelegatessupportedLocales

To use the flutter_localizations package, specify the localizationsDelegates and supportedLocales on the app widget:

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: const [
   // Add app-specific localization delegate[s] here.
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: const [
    Locale('en', 'US'), // English
    Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

委托包含实际的本地化值,而 supportedLocales 定义了应用程序支持哪些本地化。上面的示例使用了一个 MaterialApp,因此它为基本 widget 本地化值提供了一个 GlobalWidgetsLocalizations,为 Material widget 的本地化提供了一个 MaterialWidgetsLocalizations。如果你的应用程序使用 WidgetsApp ,你就不需要后者了。请注意,这两个委托包含“默认”值,但是如果您希望它们也本地化,则需要为您自己的应用程序的可本地化副本提供一个或多个委托。

The delegates contain the actual localized values, while the supportedLocales defines which locales the app supports. The above example uses a MaterialApp, so it has both a GlobalWidgetsLocalizations for the base widgets localized values, and a MaterialWidgetsLocalizations for the Material widgets localizations. If you use WidgetsApp for your app, you don’t need the latter. Note that these two delegates contain “default” values, but you’ll need to provide one or more delegates for your own app’s localizable copy, if you want those to be localized too.

初始化后, WidgetsApp (或 MaterialApp)为您创建一个 Localizations widget,其中包含您指定的委托。设备的当前区域设置总是可以从当前上下文的 Localizations widget (以 Locale 对象的形式)或使用 Window.locale 访问。

When initialized, the WidgetsApp (or MaterialApp) creates a Localizations widget for you, with the delegates you specify. The current locale for the device is always accessible from the Localizations widget from the current context (in the form of a Locale object), or using the Window.locale.

要访问本地化的资源,请使用 Localizations.of() 方法去访问一个由给定委托提供的特定本地化类。使用 intl_translation 包将可翻译的文本拷贝到 arb 文件中进行翻译,并将其导入到应用程序中与 intl 一起使用。

To access localized resources, use the Localizations.of() method to access a specific localizations class that is provided by a given delegate. Use the intl_translation package to extract translatable copy to arb files for translating, and importing them back into the app for using them with intl.

要了解更多关于 Flutter 国际化和本地化的细节,请查阅 国际化指南,它有带和不带 intl 包的示例代码。

For further details on internationalization and localization in Flutter, see the internationalization guide, which has sample code with and without the intl package.

我的项目文件在哪里?

Where is my project file?

Xamarin.Forms 中有一个 csproj 文件。在 Flutter 中最接近的它的是 pubspec.yaml,其中包含包依赖项和各种项目细节。就像 .NET Standard,相同目录中的文件被认为是项目的一部分。

In Xamarin.Forms you will have a csproj file. The closest equivalent in Flutter is pubspec.yaml, which contains package dependencies and various project details. Similar to .NET Standard, files within the same directory are considered part of the project.

Nuget 的等价物是什么?如何添加依赖项?

What is the equivalent of Nuget? How do I add dependencies?

在 .NET 生态系统中,原生 Xamarin 项目和 Xamarin.Forms 项目都可以访问 Nuget 和内置的包管理系统。 Flutter 应用程序包含一个原生Android 应用程序,原生 iOS 应用程序和 Flutter 应用程序。

In the .NET eco-system, native Xamarin projects and Xamarin.Forms projects had access to Nuget and the inbuilt package management system. Flutter apps contain a native Android app, native iOS app and Flutter app.

在Android中,您可以通过向Gradle添加构建脚本来添加依赖项。而在iOS中,你可以通过添加到 Podfile 来添加依赖项。

In Android, you add dependencies by adding to your Gradle build script. In iOS, you add dependencies by adding to your Podfile.

Flutter 使用 Dart 自己的构建系统和 Pub 包管理器。这些工具将原生 Android 和 iOS 封装应用程序的构建委托给各自的构建系统。

Flutter uses Dart’s own build system, and the Pub package manager. The tools delegate the building of the native Android and iOS wrapper apps to the respective build systems.

一般来说,使用 pubspec.yaml 来声明要在 Flutter 中使用的外部依赖项。 pub.dev 是一个寻找 Flutter 包的好地方。

In general, use pubspec.yaml to declare external dependencies to use in Flutter. A good place to find Flutter packages is on pub.dev.

应用程序生命周期

Application lifecycle

如何侦听应用程序的生命周期事件?

How do I listen to application lifecycle events?

在 Xamarin.Forms 中,拥有一个包含 OnStartOnResumeOnSleep应用程序。在 Flutter 中,您可以通过挂钩到 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 更改事件来监听类似的生命周期事件。

In Xamarin.Forms, you have an Application that contains OnStart, OnResume and OnSleep. In Flutter you can instead listen to similar lifecycle events by hooking into the WidgetsBinding observer and listening to the didChangeAppLifecycleState() change event.

可观察的生命周期事件有:

The observable lifecycle events are:

`inactive`

* `inactive` — 应用程序处于非活动状态,并且没有接收用户输入。此事件仅适用于iOS。

The application is in an inactive state and is not receiving user input. This event is iOS only.

`paused`

应用程序当前对用户不可见,不响应用户输入,但是在后台运行。

The application is not currently visible to the user, is not responding to user input, but is running in the background.

`resumed`

应用程序是可见的,并响应用户输入。

The application is visible and responding to user input.

`suspending`

应用程序暂时暂停。此事件仅限Android。

The application is suspended momentarily. This event is Android only.

有关这些状态的含义的更多细节,可参考 AppLifecycleStatus 文档

For more details on the meaning of these states, see the AppLifecycleStatus documentation.

布局

Layouts

什么东西与 StackLayout 等效?

What is the equivalent of a StackLayout?

在 Xamarin.Forms 中,可以创建一个带水平或垂直方向StackLayout 。 Flutter 也有类似的方法,不过您将使用 RowColumn widget。

In Xamarin.Forms you can create a StackLayout with an Orientation of horizontal or vertical. Flutter has a similar approach, however you would use the Row or Column widgets.

如果您注意到除了“Row” 和“Column” widget 之外,这两个代码示例是相同的。这些子元素是相同的,可以利用这个特性开发丰富的布局,这些布局可以随着时间的推移而改变。

If you notice the two code samples are identical with the exception of the “Row” and “Column” widget. The children are the same and this feature can be exploited to develop rich layouts that can change overtime with the same children.

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );
}

什么东西与网格(Grid)等价?

What is the equivalent of a Grid?

Grid最接近的对等项是 GridView。这比您在 Xamarin.Forms 中习惯使用的功能强大得多。 GridView 在内容超出其可视空间时自动滚动。

The closest equivalent of a Grid would be a GridView. This is much more powerful than what you are used to in Xamarin.Forms. A GridView provides automatic scrolling when the content exceeds its viewable space.

  GridView.count(
    // Create a grid with 2 columns. If you change the scrollDirection to
    // horizontal, this would produce 2 rows.
    crossAxisCount: 2,
    // Generate 100 widgets that display their index in the List
    children: List.generate(100, (index) {
      return Center(
        child: Text(
          'Item $index',
          style: Theme.of(context).textTheme.headline,
        ),
      );
    }),
  );

您可能在 Xamarin.Forms 中使用 Grid 来实现覆盖其他 widget 的 widget。在 Flutter 中,您可以使用 Stack widget 来完成这一操作。

You might have used a Grid in Xamarin.Forms to implement widgets that overlay other widgets. In Flutter, you accomplish this with the Stack widget

这个示例创建了两个相互重叠的图标。

This sample creates two icons that overlap each other.

child: Stack(
  children: <Widget>[
    Icon(Icons.add_box, size: 24.0, color: const Color.fromRGBO(0,0,0,1.0)),
    Positioned(
      left: 10.0,
      child: Icon(Icons.add_circle, size: 24.0, color: const Color.fromRGBO(0,0,0,1.0)),
    ),
  ],
),

有什么等同于 ScrollView ?

What is the equivalent of a ScrollView?

在 Xamarin.Forms 中,ScrollView 封装了 VisualElement,如果内容大于设备屏幕,它就会滚动。

In Xamarin.Forms, a ScrollView wraps around a VisualElement and, if the content is larger than the device screen, it scrolls.

在 Flutter 中,最接近的是 SingleChildScrollView widget。您只需用想要可滚动的内容来填充 widget。

In Flutter, the closest match is the SingleChildScrollView widget. You simply fill the Widget with the content that you want to be scrollable.

@override
Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Text('Long Content'),
  );
}

如果您想在滚动条中包含许多项,即使是不同的Widget类型,也可以使用 ListView。这可能看起来有点过火,但在 Flutter 中,它比 Xamarin.Forms 的回到平台特定控件的 ListView 优化得多,松散得多。

If you have many items you want to wrap in a scroll, even of different Widget types, you might want to use a ListView. This might seem like overkill, but in Flutter this is far more optimized and less intensive than a Xamarin.Forms ListView, which is backing on to platform specific controls.

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

在 Flutter 中如何处理横向过渡 ?

How do I handle landscape transitions in Flutter?

通过在 AndroidManifest.xml 中设置 configChanges 属性,可以自动处理横向转换。

Landscape transitions can be handled automatically by setting the configChanges property in the AndroidManifest.xml:

android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

Gesture detection and touch event handling

如何在Flutter中向 widget 添加手势识别器?

How do I add GestureRecognizers to a widget in Flutter?

在 Xamarin.Forms 中,元素(Element) 可能包含一个可供附加(attach)的单击事件。许多元素还包含一个与此事件关联的 命令 。或者你可以使用 TapGestureRecognizer。在 Flutter 中有两种非常相似的方式:

In Xamarin.Forms, Elements might contain a click event you can attach to. Many elements also contain a Command that is tied to this event. Alternatively you would use the TapGestureRecognizer. In Flutter there are two very similar ways:

  1. 如果 widget 支持事件发现(detection),那么可以将函数传递给它并在函数中处理它:

    If the widget supports event detection, pass a function to it and handle it in the function. For example, the ElevatedButton has an onPressed parameter:

    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
          onPressed: () {
            print("click");
          },
          child: Text("Button"));
    }
    
  2. If the widget doesn’t support event detection, wrap the widget in a GestureDetector and pass a function to the onTap parameter.

如果 widget 不支持事件发现,则将 widget 封装在手势检测器(GestureDetector)中,并将函数传递给 onTap 参数。

<!-- skip -->
```dart
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}
```

我如何处理 widget 上的其他手势?

How do I handle other gestures on widgets?

在 Xamarin.Forms 中你可以在 VisualElement 中添加一个 手势识别器(GestureRecognizer)。您通常只能使用 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizer、, SwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非您构建了自己的实现。

In Xamarin.Forms you would add a GestureRecognizer to the View. You would normally be limited to TapGestureRecognizer, PinchGestureRecognizer, PanGestureRecognizer, SwipeGestureRecognizer, DragGestureRecognizer and DropGestureRecognizer unless you built your own.

在Flutter中,使用手势检测器,你可以监听到各种各样的手势,比如:

In Flutter, using the GestureDetector, you can listen to a wide range of Gestures such as:

`onTapDown`

当指尖在特定位置与屏幕接触产生点击事件。

A pointer that might cause a tap has contacted the screen at a particular location.

`onTapUp`

当指尖触发的点击事件已经停止在特定位置与屏幕接触。

A pointer that triggers a tap has stopped contacting the screen at a particular location.

`onTap`

一个点击事件已经发生

A tap has occurred.

`onTapCancel`

触发了 `onTapDown` 事件之后的指尖没有导致点击事件。

The pointer that previously triggered the `onTapDown` won't cause a tap.

`onDoubleTap`

用户在同一位置连续快速点击屏幕两次。

The user tapped the screen at the same location twice in quick succession.

`onLongPress`

指尖长时间保持与屏幕在同一位置的接触。

A pointer has remained in contact with the screen at the same location for a long period of time.

`onVerticalDragStart`

指尖与屏幕接触后,可能开始垂直移动。

A pointer has contacted the screen and might begin to move vertically.

`onVerticalDragUpdate`

指尖与屏幕接触并在垂直方向上移动得更远。

A pointer in contact with the screen has moved further in the vertical direction.

`onVerticalDragEnd`

指尖在之前与屏幕接触并垂直移动,当不再与屏幕接触时触发这个事件。当它停止与屏幕接触时,它会以特定的速度移动。

A pointer that was previously in contact with the screen and moving vertically is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.

`onHorizontalDragStart`

指尖与屏幕接触,开始水平移动时触发。

A pointer has contacted the screen and might begin to move horizontally.

`onHorizontalDragUpdate`

指尖与屏幕接触并在水平方向上移动得更远。

A pointer in contact with the screen has moved further in the horizontal direction.

`onHorizontalDragEnd`

指尖在之前与屏幕接触并水平移动,当不再与屏幕接触时会触发这个事件。当它停止与屏幕接触时,它正在以特定的速度移动。

A pointer that was previously in contact with the screen and moving horizontally is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.

下面的例子展示了一个手势检测器,它可以在双击下旋转 Flutter 的 logo:

The following example shows a GestureDetector that rotates the Flutter logo on a double tap:

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  super.initState();
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: GestureDetector(
            child: RotationTransition(
                turns: curve,
                child: FlutterLogo(
                  size: 200.0,
                )),
            onDoubleTap: () {
              if (controller.isCompleted) {
                controller.reverse();
              } else {
                controller.forward();
              }
            },
        ),
    ));
  }
}

列表视图和适配器

Listviews and adapters

在 Flutter 中,与列表视图等价的是什么?

What is the equivalent to a ListView in Flutter?

在Flutter中与 ListView 等价的是……一个 ListView

The equivalent to a ListView in Flutter is … a ListView!

在一个 Xamarin.Forms 的 ListView 中,你可以创建一个 ViewCell 可能还有一个 DataTemplateSelector 并将其传递到 ListView 中,该视图将用您的DataTemplateSelector 或者 ViewCell 的返回数据渲染每一行。但是,您通常必须确保打开单元格回收,否则会遇到内存问题和会使滚动速度变慢。

In a Xamarin.Forms ListView, you create a ViewCell and possibly a DataTemplateSelector and pass it into the ListView, which renders each row with what your DataTemplateSelector or ViewCell returns. However, you often have to make sure you turn on Cell Recycling otherwise you will run into memory issues and slow scrolling speeds.

由于 Flutter 的不可变的 widget 模式,您将一个 widget 列表传递给您的 ListView,Flutter 会负责确保滚动速度快且平稳。

Due to Flutter’s immutable widget pattern, you pass a list of widgets to your ListView, and Flutter takes care of making sure that scrolling is fast and smooth.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

如何知道哪个列表项被点击了?

How do I know which list item has been clicked?

在 Xamarin.Forms 中,ListView 拥有一个ItemTapped 方法能找出哪个列表项被单击了。您可能还使用了许多其他技术,比如检查 SelectedItemEventToCommand 的行为何时会发生更改。

In Xamarin.Forms, the ListView has an ItemTapped method to find out which item was clicked. There are many other techniques you might have used such as checking when SelectedItem or EventToCommand behaviors change.

在 Flutter 中,使用传入 widget 提供的触摸处理。

In Flutter, use the touch handling provided by the passed-in widgets.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("Row $i")),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

如何动态更新 ListView ?

How do I update a ListView dynamically?

在 Xamarin.Forms 中,如果将 ItemsSource 属性绑定到一个 ObservableCollection,就只需要更新视图模型中的列表。另一种方法是,你可以给属性 ItemsSource 分配一个新的 列表

In Xamarin.Forms, if you bound the ItemsSource property to an ObservableCollection you would just update the list in your ViewModel. Alternatively, you could assign a new List to the ItemSource property.

在 Flutter 中,情况略有不同。如果您要在 setState() 内更新 widget 列表,您将很快看到您的数据在视觉上没有发生变化。这是因为当 setState() 被调用时,Flutter 的渲染引擎会检查 widget 树是否发生了更改。当它到达您的 ListView 时,会执行 == 检查,并确定这两个 ListView 是相同的。没有任何更改,就不需要更新。

In Flutter, things work a little differently. If you update the list of widgets inside a setState() method, you would quickly see that your data did not change visually. This is because when setState() is called, the Flutter rendering engine looks at the widget tree to see if anything has changed. When it gets to your ListView, it performs a == check, and determines that the two ListViews are the same. Nothing has changed, so no update is required.

要更新 ListView 的有一个简单方法,请在 setState() 中创建一个新 列表 ,并将数据从旧列表复制到新列表。虽然这种方法很简单,但不推荐用于大型数据集,如下例所示。

For a simple way to update your ListView, create a new List inside of setState(), and copy the data from the old list to the new list. While this approach is simple, it is not recommended for large data sets, as shown in the next example.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
    );
  }
}

推荐的、高效的、有效的列表构建方法是使用 ListView.Builder。在您有一个动态列表或一个包含大量数据的列表时,这种方法非常棒。这基本上相当于 Android 上的 RecyclerView,它会自动回收列表元素:

The recommended, efficient, and effective way to build a list uses a ListView.Builder. This method is great when you have a dynamic list or a list with very large amounts of data. This is essentially the equivalent of RecyclerView on Android, which automatically recycles list elements for you:

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: ListView.builder(
            itemCount: widgets.length,
            itemBuilder: (BuildContext context, int position) {
              return getRow(position);
            }));
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
    );
  }
}

与创建一个“列表视图” 相比,创建一个 ListView.builder 需要接受两个关键参数:列表的初始长度和 ItemBuilder 函数。

Instead of creating a “ListView”, create a ListView.builder that takes two key parameters: the initial length of the list, and an ItemBuilder function.

ItemBuilder 函数类似于 Android 适配器中的 getView 函数;它接受一个位置,并返回您希望的在该位置呈现的行。

The ItemBuilder function is similar to the getView function in an Android adapter; it takes a position, and returns the row you want rendered at that position.

最后,但也是最重要的,要注意 onTap() 函数不再重新创建列表,而是用 .add 添加给它的。

Finally, but most importantly, notice that the onTap() function doesn’t recreate the list anymore, but instead adds to it.

更多信息,请访问 编写你的第一个 Flutter 应用程序,第1部分编写你的第一个 Flutter 应用程序,第2部分

For more information, see Write your first Flutter app, part 1 and Write your first Flutter app, part 2.

文本处理

Working with text

如何在文本(Text) widget 上设置自定义字体?

How do I set custom fonts on my text widgets?

在 Xamarin.Forms 中,您必须在每个原生项目中添加自定义字体。然后在你的 元素 中,你会使用 filename#fontnameFontFamily 属性分配这个字体名,而在iOS中只使用 fontname

In Xamarin.Forms, you would have to add a custom font in each native project. Then, in your Element you would assign this font name to the FontFamily attribute using filename#fontname and just fontname for iOS.

在 Flutter 中,将字体文件放在一个文件夹中,并在 pubspec.yaml 中引用它,这跟导入图像的方式类似。

In Flutter, place the font file in a folder and reference it in the pubspec.yaml file, similar to how you import images.

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

Then assign the font to your Text widget:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何设置文本 widget 的样式?

How do I style my text widgets?

除了字体,您还可以在文本 widget 上定制其他样式元素。 文本 widget 的样式参数接受一个 TextStyle 对象,您可以在其中定制许多参数,比如:

Along with fonts, you can customize other styling elements on a Text widget. The style parameter of a Text widget takes a TextStyle object, where you can customize many parameters, such as:

表单录入

Form input

如何检索用户输入?

How do I retrieve user input?

Xamarin.Forms 的元素允许您直接查询元素来确定它的任何属性的状态,或者它被绑定到视图模型中的属性。

Xamarin.Forms elements allow you to directly query the element to determine the state of any of its properties, or whether it’s bound to a property in a ViewModel.

在 Flutter 中检索信息是由专门的 widget 处理的,这是跟原来的习惯不同的。如果你有一个 TextFieldTextFormField ,你可以提供一个 TextEditingController 来检索用户输入:

Retrieving information in Flutter is handled by specialized widgets and is different than how you are used to. If you have a TextField or a TextFormField, you can supply a TextEditingController to retrieve user input:

class _MyFormState extends State<MyForm> {
  // Create a text controller and use it to retrieve the current value
  // of the TextField.
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: myController,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text that the user has typed into our text field.
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text that the user has entered using the
                // TextEditingController.
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: Icon(Icons.text_fields),
      ),
    );
  }
}

你可以在 Flutter 实用教程 中的 获取文本框的输入值 找到更多的信息和完整的代码清单。

You can find more information and the full code listing in Retrieve the value of a text field, from the Flutter cookbook.

在入口的占位符 (Placeholder) 与什么等价?

What is the equivalent of a Placeholder on an Entry?

在 Xamarin.Forms 中,一些元素支持占位符(Placeholder)属性,可以给它赋一个值。如:

In Xamarin.Forms, some Elements support a Placeholder property that you can assign a value to. For example:

  <Entry Placeholder="This is a hint">

在 Flutter 中,通过在文本 widget 的装饰器构造函数参数中添加 InputDecoration 对象,可以轻松地为输入显示“提示”或占位符文本。

In Flutter, you can easily show a “hint” or a placeholder text for your input by adding an InputDecoration object to the decoration constructor parameter for the text widget.

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  )
)

如何显示验证错误?

How do I show validation errors?

使用 Xamarin.Forms 时,如果您希望提供验证错误的可视化提示,则需要创建新属性和 虚拟元素(VisualElement) 来包围具有验证错误的元素。

With Xamarin.Forms, if you wished to provide a visual hint of a validation error, you would need to create new properties and VisualElements surrounding the Elements that had validation errors.

在 Flutter 中,我们将 InputDecoration 对象传递给文本 widget 的装饰器构造函数。

In Flutter, you pass through an InputDecoration object to the decoration constructor for the text widget.

然而,您不希望从显示错误开始。相反,当用户输入无效数据时,应该更新状态,并传递一个新的 InputDecoration 对象。

However, you don’t want to start off by showing an error. Instead, when the user has entered invalid data, update the state, and pass a new InputDecoration object.

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

Flutter 插件

Flutter plugins

与硬件、第三方服务和平台交互

Interacting with hardware, third party services, and the platform

应该如何与平台以及平台原生代码交互?

How do I interact with the platform, and with platform native code?

Flutter 不直接在底层平台上运行代码。相反,构成一个 Flutter 应用程序的 Dart 代码是在设备上原生运行的,“绕开”了平台提供的 SDK。这意味着,例如,当您在 Dart 中执行网络请求时,它将直接运行在 Dart 上下文中。在编写原生应用程序时,您通常不会使用 Android 或 iOS 的 API。 Flutter 应用程序仍然作为视图驻留在原生应用程序的 ViewControllerActivity 中,但您不能直接访问这个或原生框架。

Flutter doesn’t run code directly on the underlying platform; rather, the Dart code that makes up a Flutter app is run natively on the device, “sidestepping” the SDK provided by the platform. That means, for example, when you perform a network request in Dart, it runs directly in the Dart context. You don’t use the Android or iOS APIs you normally take advantage of when writing native apps. Your Flutter app is still hosted in a native app’s ViewController or Activity as a view, but you don’t have direct access to this, or the native framework.

这并不意味着 Flutter 应用程序不能与这些原生 API 或您自己的任何原生代码交互。 Flutter 提供 平台通道, 可以与托管 Flutter 视图的 ViewControllerActivity 通信和交换数据。平台通道本质上是一个异步消息传递机制,它将 Dart 代码与 ViewControllerActivity 宿主以及它所运行的 iOS 或 Android 框架桥接起来。例如,您可以使用平台通道在原生端执行一个方法,或者从设备的传感器检索一些数据。

This doesn’t mean Flutter apps can’t interact with those native APIs, or with any native code you have. Flutter provides platform channels that communicate and exchange data with the ViewController or Activity that hosts your Flutter view. Platform channels are essentially an asynchronous messaging mechanism that bridges the Dart code with the host ViewController or Activity and the iOS or Android framework it runs on. You can use platform channels to execute a method on the native side, or to retrieve some data from the device’s sensors, for example.

除了直接使用平台通道外,您还可以使用各种预制 插件,它们封装了针对特定目标的原生代码和Dart代码。例如,您可以使用插件直接从Flutter访问相机交卷和设备相机,而无需编写自己的集成。插件可以在 pub.dev、Dart 和 Flutter 的开源包存储库中找到。有些包可能支持iOS上的本地集成,有些支持Android,还有两者都兼而有之的。

In addition to directly using platform channels, you can use a variety of pre-made plugins that encapsulate the native and Dart code for a specific goal. For example, you can use a plugin to access the camera roll and the device camera directly from Flutter, without having to write your own integration. Plugins are found on pub.dev, Dart and Flutter’s open source package repository. Some packages might support native integrations on iOS, or Android, or both.

如果在 Pub 上找不到适合您需求的插件,你可以 编写自己的插件在 Pub 上发布

If you can’t find a plugin on pub.dev that fits your needs, you can write your own, and publish it on pub.dev.

如何访问 GPS 传感器?

How do I access the GPS sensor?

使用 geolocator 社区插件.

Use the geolocator community plugin.

如何访问摄相机?

How do I access the camera?

image_picker 是流行的访问相机的插件。

The image_picker plugin is popular for accessing the camera.

如何通过 Facebook 登录?

How do I log in with Facebook?

To log in with Facebook, use the

使用 flutter_facebook_login 社区插件来通过 Facebook 登录。

flutter_facebook_login community plugin.

如何使用 Firebase 特性?

How do I use Firebase features?

大多数 Firebase 功能被 官方插件 覆盖。

Most Firebase functions are covered by first party plugins. These plugins are first-party integrations, maintained by the Flutter team:

你也可以在 Pub 上找一些第三方 Firebase 插件,它们覆盖了第一方插件没有直接覆盖的区域。

You can also find some third-party Firebase plugins on pub.dev that cover areas not directly covered by the first-party plugins.

如何构建自定义的原生集成?

How do I build my own custom native integrations?

如果有 Flutter 或它的社区插件没有的指定平台的功能,可以根据 开发包与插件 页面自己构建。

If there is platform-specific functionality that Flutter or its community plugins are missing, you can build your own following the developing packages and plugins page.

简单地说,Flutter 的插件架构很像在 Android 中使用事件总线:您发出一条消息,让接收方处理并向您发回一个结果。在这个例子中,接收方是运行在 Android 或 iOS 上的原生代码。

Flutter’s plugin architecture, in a nutshell, is much like using an Event bus in Android: you fire off a message and let the receiver process and emit a result back to you. In this case, the receiver is code running on the native side on Android or iOS.

主题(样式)

Themes (Styles)

如何美化我的应用程序?

How do I theme my app?

Flutter 附带了一个内建的漂亮的 Material Design 实现,它处理了许多您通常会做的样式和主题需求

Flutter comes with a beautiful, built-in implementation of Material Design, which handles much of the styling and theming needs that you would typically do.

Xamarin.Forms 确实有一个全局的 资源字典,可以为你的应用程序共享样式。另外,预览版目前还支持主题。

Xamarin.Forms does have a global ResourceDictionary where you can share styles across your app. Alternatively, there is Theme support currently in preview.

在 Flutter 中,需要在最顶级 widget 中声明主题。

In Flutter you declare themes in the top level widget.

要在应用程序中充分利用 Material 组件,需要声明一个最顶级 widget MaterialApp 作为应用程序的入口点。 MaterialApp 是一个方便的 widget,它封装了许多实现Material Design的应用程序通常需要的各种 widget。它通过添加 Material 的指定功能来构建一个 WidgetsApp。

To take full advantage of Material Components in your app, you can declare a top level widget MaterialApp as the entry point to your application. MaterialApp is a convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design. It builds upon a WidgetsApp by adding Material-specific functionality.

还可以使用一个 WidgetApp 作为应用程序的 widget,它提供了一些相同的功能,但没有 MaterialApp 丰富。

You can also use a WidgetsApp as your app widget, which provides some of the same functionality, but is not as rich as MaterialApp.

要定制任何子组件的颜色和样式,请将主题数据(ThemeData)对象传递给MaterialApp widget。例如,在下面的代码中,主色调设置为蓝色,文本选择颜框色为红色。

To customize the colors and styles of any child components, pass a ThemeData object to the MaterialApp widget. For example, in the following code, the primary swatch is set to blue and text selection color is red.

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}

数据库与本地存储

Databases and local storage

如何访问共享首选项或用户默认值?

How do I access shared preferences or UserDefaults?

Xamarin.Forms 开发者可能会熟悉 Xam.Plugins.Settings 插件。

Xamarin.Forms developers will likely be familiar with the Xam.Plugins.Settings plugin.

在 Flutter 中,使用 Shared Preferences 插件 就可以访问相同的功能。这个插件封装了 用户默认值 和等同 Android 的 共享首选项

In Flutter, access equivalent functionality using the shared_preferences plugin. This plugin wraps the functionality of both UserDefaults and the Android equivalent, SharedPreferences.

在 Flutter 中如何访问 SQLite

How do I access SQLite in Flutter?

在 Xamarin.Forms 中大多数应用会使用 sqlite-net-pcl 插件来访问 SQLite 数据库。

In Xamarin.Forms most applications would use the sqlite-net-pcl plugin to access SQLite databases.

在 Flutter 中,使用 SQFlite 插件来访问这个功能。

In Flutter, access this functionality using the sqflite plugin.

调试

Debugging

应该使用什么工具调试我的 Flutter 应用?

What tools can I use to debug my app in Flutter?

请使用 开发者工具 debug 你的 Flutter 和 Dart 应用。

Use the DevTools suite for debugging Flutter or Dart apps.

开发者工具包含了 profiling 构建、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 开发者工具 文档。

DevTools includes support for profiling, examining the heap, inspecting the widget tree, logging diagnostics, debugging, observing executed lines of code, debugging memory leaks and memory fragmentation. For more information, see the DevTools documentation.

通知

Notifications

如何设置通知推送?

How do I set up push notifications?

在 Android 中,你可以利用 Firebase Cloud Messaging 来给应用程序设置通知推送。

In Android, you use Firebase Cloud Messaging to setup push notifications for your app.

在 Flutter 中,通过 Firebase_Messaging 插件来访问这个功能。更多关于使用 Firebase Cloud Messaging API 的信息,可以参考 firebase_messaging 插件文档。

In Flutter, access this functionality using the Firebase_Messaging plugin. For more information on using the Firebase Cloud Messaging API, see the firebase_messaging plugin documentation.

声明式 UI 介绍

目录

这篇介绍描述了 Flutter 所使用的声明式 UI 和许多其他 UI 框架所使用的命令式 UI 的概念性差异

This introduction describes the conceptual difference between the declarative style used by Flutter, and the imperative style used by many other UI frameworks.

为什么是声明式 UI?

Why a declarative UI?

从 Win32 到 Web 再到 Android 和 iOS,框架通常使用一种命令式的编程风格来完成 UI 编程。这可能是你最熟悉的风格 — 你手动构建一个全功能的 UI 实例,比如一个 UIView 或其他类似的,在随后 UI 发生变化时,使用方法或 Setter 修改它。

Frameworks from Win32 to web to Android and iOS typically use an imperative style of UI programming. This might be the style you’re most familiar with—where you manually construct a full-functioned UI entity, such as a UIView or equivalent, and later mutate it using methods and setters when the UI changes.

为了减轻开发人员的负担,无需编写如何在不同的 UI 状态之间进行切换的代码, Flutter 相反,让开发人员描述当前的 UI 状态,并将转换交给框架。

In order to lighten the burden on developers from having to program how to transition between various UI states, Flutter, by contrast, lets the developer describe the current UI state and leaves the transitioning to the framework.

然而,这需要稍微改变下如何操作 UI 的思考方式

This, however, requires a slight shift in thinking for how to manipulate UI.

如何在命令式框架中修改 UI

How to change UI in a declarative framework

思考像下面这样一个简单的例子:

Consider a simplified example below:

View B (contained by view A) morphs from containing two views, c1 and c2, to containing only view c3

在命令式风格中,你通常需要使用选择器 findViewById 或类似函数获取到 ViewB 的实例 b 和所有权,并调用相关的修改的方法(并隐式的使其失效)。例如:

In the imperative style, you would typically go to ViewB’s owner and retrieve the instance b using selectors or with findViewById or similar, and invoke mutations on it (and implicitly invalidate it). For example:

// Imperative style
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)

由于 UI 真实的来源可能比实例 b 本身的存活周期更长,你可能还需要在 ViewB 的构造函数中复制此配置。

You might also need to replicate this configuration in the constructor of ViewB since the source of truth for the UI might outlive instance b itself.

在声明式风格中,视图配置(如 Flutter 的 Widget )是不可变的,它只是轻量的“蓝图”。要改变 UI,widget 会在自身上触发重建(在 Flutter 中最常见的方法是在 StatefulWidgets 组件上调用 setState())并构造一个新的 Widget 子树。

In the declarative style, view configurations (such as Flutter’s Widgets) are immutable and are only lightweight “blueprints”. To change the UI, a widget triggers a rebuild on itself (most commonly by calling setState() on StatefulWidgets in Flutter) and constructs a new Widget subtree.

// Declarative style
return ViewB(
  color: red,
  child: ViewC(...),
)

在这里,当用户界面发生变化时,Flutter 不会修改旧的实例 b,而是构造新的 widget 实例。框架使用 RenderObjects 管理传统 UI 对象的职责(比如维护布局的状态)。 RenderObjects 在帧之间保持不变, Flutter 的轻量级 widget 通知框架在状态之间修改 RenderObjects, Flutter 框架则处理其余部分。

Here, rather than mutating an old instance b when the UI changes, Flutter constructs new Widget instances. The framework manages many of the responsibilities of a traditional UI object (such as maintaining the state of the layout) behind the scenes with RenderObjects. RenderObjects persist between frames and Flutter’s lightweight Widgets tell the framework to mutate the RenderObjects between states. The Flutter framework handles the rest.

Building a web application with Flutter

目录

This page covers the following steps for getting started with web support:

Requirements

To create a Flutter app with web support, you need the following software:

For more information, see the web FAQ.

Create a new project with web support

You can use the following steps to create a new project with web support.

Set up

Run the following commands to use the latest version of the Flutter SDK:

$ flutter channel stable
$ flutter upgrade

If Chrome is installed, the flutter devices command outputs a Chrome device that opens the Chrome browser with your app running, and a Web Server that provides the URL serving the app.

$ flutter devices
1 connected device:

Chrome (web) • chrome • web-javascript • Google Chrome 88.0.4324.150

In your IDE, you should see Chrome (web) in the device pulldown.

Create and run

Creating a new project with web support is no different than creating a new Flutter project for other platforms.

IDE

Create a new app in your IDE and it automatically creates iOS, Android, and web versions of your app. (And macOS, too, if you’ve enabled desktop support.) From the device pulldown, select Chrome (web) and run your app to see it launch in Chrome.

Command line

To create a new app that includes web support (in addition to mobile support), run the following commands, substituting myapp with the name of your project:

$ flutter create myapp
$ cd myapp

To serve your app from localhost in Chrome, enter the following from the top of the package:

$ flutter run -d chrome

The flutter run command launches the application using the development compiler in a Chrome browser.

Build

Run the following command to generate a release build:

$ flutter build web

A release build uses dart2js (instead of the development compiler) to produce a single JavaScript file main.dart.js. You can create a release build using release mode (flutter run --release) or by using flutter build web. This populates a build/web directory with built files, including an assets directory, which need to be served together.

You can also include --web-renderer html or --web-renderer canvaskit to select between the HTML or CanvasKit renderers, respectively. For more information, see Web renderers.

For more information, see Build and release a web app.

Add web support to an existing app

To add web support to an existing project created using a previous version of Flutter, run the following command from your project’s directory:

$ flutter create .

实用教程

目录

本节内容包含了一个又一个的实用教程,帮助你解决编写 Flutter 应用中的常见问题。

This cookbook contains recipes that demonstrate how to solve common problems while writing Flutter apps. Each recipe is self-contained and can be used as a reference to help you build up an application.

Animation

Design

Effects

Forms

Gestures

Images

Lists

Maintenance

Networking

Persistence

Plugins

Testing

Integration

Unit

Widget

如果想看到更多的教程,可以参考我们的 社区中文教程页面

Codelabs & workshops

目录

Flutter 的 codelabs 是一份为新手准备的入门指南。一些 codelabs 运行在 DartPad—上,这意味着你不需要下载任何东西就能够轻松学习。

Flutter workshops 与 codelabs 类似,但由讲师指导并始终使用 DartPad。 workshop 链接也会提供给你相关的 YouTube 视频,该视频会告诉你在哪里可以找到相关的 DartPad 链接。

The Flutter codelabs provide a guided, hands-on coding experience. Some codelabs run in DartPad—no downloads required!

Flutter workshops are similar to the codelabs, but are instructor led and always use DartPad. The provided workshop link takes you to the relevant YouTube video, which tells you where to find the associated DartPad link.

适用于初级开发者

Good for beginners

如果你刚开始学习 Flutter,我们推荐你学习下面的 Codelab 之一:

If you’re new to Flutter, we recommend starting with one of these codelabs:

下一步

Next steps

使用 Flutter 设计 UI

Designing a Flutter UI

了解 Material Design 和 Flutter基本概念,例如布局和动画:

Learn about Material Design and basic Flutter concepts, like layout and animations:

在 Flutter 应用中集成

Using Flutter with…

学习如何使用在 Flutter 中使用其他技术产品/平台:

Learn how to use Flutter with other technologies.

测试

Testing

学习如何测试你的 Flutter 应用:

Learn how to test your Flutter application.

撰写平台特别的代码

Writing platform-specific code

学习如何撰写在特定的平台运行的代码,比如 iOS、Android、Web 和桌面端。

Learn how to write code that’s targeted for specific platforms, like iOS, Android, the web, and the desktop.

其他资源

Other resources

查看更多 Dart 相关的 codelab,请在 Dart 网站 上查看 codelabs 页面。

下面这些线上的课程也很不错:

For Dart-specific codelabs, see the codelabs page on the Dart site.

We also recommend the following online class:

Flutter 实践教程

Flutter 实践教程,帮助你完成相对较为完整的 Flutter 小应用。

The Flutter tutorials teach you how to use the Flutter framework to build mobile applications for iOS and Android.

你可以选择以下教程:

Choose from the following:

Widgets 介绍

Flutter 从 React 中吸取灵感,通过现代化框架创建出精美的组件。它的核心思想是用 widget 来构建你的 UI 界面。 Widget 描述了在当前的配置和状态下视图所应该呈现的样子。当 widget 的状态改变时,它会重新构建其描述(展示的 UI),框架则会对比前后变化的不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。

Flutter widgets are built using a modern framework that takes inspiration from React. The central idea is that you build your UI out of widgets. Widgets describe what their view should look like given their current configuration and state. When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.

Hello world

创建一个最小的 Flutter 应用简单到仅需调用 runApp() 方法并传入一个 widget 即可:

The minimal Flutter app simply calls the runApp() function with a widget:

import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

runApp() 函数会持有传入的 Widget,并且使它成为 widget 树中的根节点。在这个例子中,Widget 树有两个 widgets, Center widget 及其子 widget —— Text widget。框架会强制让根 widget 铺满整个屏幕,也就是说“Hello World”会在屏幕上居中显示。在这个例子我们需要指定文字的方向,当使用 MaterialApp widget 时,你就无需考虑这一点,之后我们会进一步的描述。

The runApp() function takes the given Widget and makes it the root of the widget tree. In this example, the widget tree consists of two widgets, the Center widget and its child, the Text widget. The framework forces the root widget to cover the screen, which means the text “Hello, world” ends up centered on screen. The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later.

在写应用的过程中,取决于是否需要管理状态,你通常会创建一个新的组件继承 StatelessWidgetStatefulWidget。 Widget 的主要工作是实现 build() 方法,该方法根据其它较低级别的 widget 来描述这个 widget。框架会逐一构建这些 widget,直到最底层的描述 widget 几何形状的 RenderObject

When writing an app, you’ll commonly author new widgets that are subclasses of either StatelessWidget or StatefulWidget, depending on whether your widget manages any state. A widget’s main job is to implement a build() function, which describes the widget in terms of other, lower-level widgets. The framework builds those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject, which computes and describes the geometry of the widget.

基础 widgets

Basic widgets

Flutter 自带了一套强大的基础 widgets,下面列出了一些常用的:

Flutter comes with a suite of powerful basic widgets, of which the following are commonly used:

Text
Text widget 可以用来在应用内创建带样式的文本。

Text
The Text widget lets you create a run of styled text within your application.

Row, Column
这两个 flex widgets 可以让你在水平 (Row) 和垂直(Column) 方向创建灵活的布局。它是基于 web 的 flexbox 布局模型设计的。

Row, Column
These flex widgets let you create flexible layouts in both the horizontal (Row) and vertical (Column) directions. The design of these objects is based on the web’s flexbox layout model.

Stack
Stack widget 不是线性(水平或垂直)定位的,而是按照绘制顺序将 widget 堆叠在一起。你可以用 Positioned widget 作为Stack 的子 widget,以相对于 Stack 的上,右,下,左来定位它们。 Stack 是基于 Web 中的绝对位置布局模型设计的。

Stack
Instead of being linearly oriented (either horizontally or vertically), a Stack widget lets you place widgets on top of each other in paint order. You can then use the Positioned widget on children of a Stack to position them relative to the top, right, bottom, or left edge of the stack. Stacks are based on the web’s absolute positioning layout model.

Container
Container widget 可以用来创建一个可见的矩形元素。 Container 可以使用 BoxDecoration 来进行装饰,如背景,边框,或阴影等。 Container 还可以设置外边距、内边距和尺寸的约束条件等。另外,Container可以使用矩阵在三维空间进行转换。

Container
The Container widget lets you create a rectangular visual element. A container can be decorated with a BoxDecoration, such as a background, a border, or a shadow. A Container can also have margins, padding, and constraints applied to its size. In addition, a Container can be transformed in three dimensional space using a matrix.

下面是一些简单的 widget,它们结合了上面提到的 widget 和一些其他的 widget:

Below are some simple widgets that combine these and other widgets:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  const MyAppBar({required this.title, Key? key}) : super(key: key);

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        // <Widget> is the type of items in the list.
        children: [
          const IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child
          // to fill the available space.
          Expanded(
            child: title,
          ),
          const IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  const MyScaffold({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece
    // of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: [
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context) //
                  .primaryTextTheme
                  .headline6,
            ),
          ),
          const Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: 'My app', // used by the OS task switcher
      home: SafeArea(
        child: MyScaffold(),
      ),
    ),
  );
}

请确认在 pubspec.yaml 文件中 flutter 部分有 uses-material-design: true 这条,它能让你使用预置的 Material icons

Be sure to have a uses-material-design: true entry in the flutter section of your pubspec.yaml file. It allows you to use the predefined set of Material icons. It’s generally a good idea to include this line if you are using the Materials library.

name: my_app
flutter:
  uses-material-design: true

为了获得(MaterialApp)主题的数据,许多 Material Design 的 widget 需要在 MaterialApp 中才能显现正常。因此,请使用 MaterialApp 运行应用。

Many Material Design widgets need to be inside of a MaterialApp to display properly, in order to inherit theme data. Therefore, run the application with a MaterialApp.

MyAppBar widget 创建了一个高 56 独立像素,左右内边距 8 像素的 Container。在容器内,MyAppBarRow 布局来组织它的子元素。中间的子 widget(title widget),被标记为 Expanded,这意味着它会扩展以填充其它子 widget 未使用的可用空间。你可以定义多个Expanded 子 widget,并使用 flex 参数确定它们占用可用空间的比例。

The MyAppBar widget creates a Container with a height of 56 device-independent pixels with an internal padding of 8 pixels, both on the left and the right. Inside the container, MyAppBar uses a Row layout to organize its children. The middle child, the title widget, is marked as Expanded, which means it expands to fill any remaining available space that hasn’t been consumed by the other children. You can have multiple Expanded children and determine the ratio in which they consume the available space using the flex argument to Expanded.

MyScaffold widget 将其子 widget 组织在垂直列中。在列的顶部,它放置一个 MyAppBar 实例,并把 Text widget 传给它来作为应用的标题。把 widget 作为参数传递给其他 widget 是一个很强大的技术,它可以让你以各种方式创建一些可重用的通用组件。最后,MyScaffold 使用 Expanded 来填充剩余空间,其中包含一个居中的消息。

The MyScaffold widget organizes its children in a vertical column. At the top of the column it places an instance of MyAppBar, passing the app bar a Text widget to use as its title. Passing widgets as arguments to other widgets is a powerful technique that lets you create generic widgets that can be reused in a wide variety of ways. Finally, MyScaffold uses an Expanded to fill the remaining space with its body, which consists of a centered message.

有关更多信息,请参阅 布局

For more information, see Layouts.

使用 Material 组件

Using Material Components

Flutter 提供了许多 widget,可帮助你构建遵循 Material Design 的应用。 Material 应用以 MaterialApp widget 开始,它在你的应用的底层下构建了许多有用的 widget。这其中包括 Navigator,它管理由字符串标识的 widget 栈,也称为“routes”。 Navigator 可以让你在应用的页面中平滑的切换。使用 MaterialApp widget 不是必须的,但这是一个很好的做法。

Flutter provides a number of widgets that help you build apps that follow Material Design. A Material app starts with the MaterialApp widget, which builds a number of useful widgets at the root of your app, including a Navigator, which manages a stack of widgets identified by strings, also known as “routes”. The Navigator lets you transition smoothly between screens of your application. Using the MaterialApp widget is entirely optional but a good practice.

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Flutter Tutorial',
      home: TutorialHome(),
    ),
  );
}

class TutorialHome extends StatelessWidget {
  const TutorialHome({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for
    // the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: const Text('Example title'),
        actions: const [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: const Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: const FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

现在我们已经从 MyAppBarMyScaffold 切换到了 material.dart 中的 AppBarScaffold widget,我们的应用更“Material”了一些。例如,标题栏有了阴影,标题文本会自动继承正确的样式,此外还添加了一个浮动操作按钮。

Now that the code has switched from MyAppBar and MyScaffold to the AppBar and Scaffold widgets, and from material.dart, the app is starting to look a bit more Material. For example, the app bar has a shadow and the title text inherits the correct styling automatically. A floating action button is also added.

注意,widget 作为参数传递给了另外的 widget。 Scaffold widget 将许多不同的 widget 作为命名参数,每个 widget 都放在了 Scofford 布局中的合适位置。同样的,AppBar widget 允许我们给 leadingtitle widget 的 actions 传递 widget。这种模式在整个框架会中重复出现,在设计自己的 widget 时可以考虑这种模式。

Notice that widgets are passed as arguments to other widgets. The Scaffold widget takes a number of different widgets as named arguments, each of which are placed in the Scaffold layout in the appropriate place. Similarly, the AppBar widget lets you pass in widgets for the leading widget, and the actions of the title widget. This pattern recurs throughout the framework and is something you might consider when designing your own widgets.

有关更多信息,请参阅 Material 组件

For more information, see Material Components widgets.

处理手势

Handling gestures

大多数应用都需要通过系统来处理一些用户交互。构建交互式应用程序的第一步是检测输入手势,这里通过创建一个简单的按钮来了解其工作原理:

Most applications include some form of user interaction with the system. The first step in building an interactive application is to detect input gestures. See how that works by creating a simple button:

import 'package:flutter/material.dart';

class MyButton extends StatelessWidget {
  const MyButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: const Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ),
  );
}

GestureDetector widget 没有可视化的展现,但它能识别用户的手势。当用户点击 Container 时, GestureDetector 会调用其 onTap() 回调,在这里会向控制台打印一条消息。你可以使用 GestureDetector 检测各种输入的手势,包括点击,拖动和缩放。

The GestureDetector widget doesn’t have a visual representation but instead detects gestures made by the user. When the user taps the Container, the GestureDetector calls its onTap() callback, in this case printing a message to the console. You can use GestureDetector to detect a variety of input gestures, including taps, drags, and scales.

许多 widget 使用 GestureDetector 为其他 widget 提供可选的回调。例如,IconButtonElevatedButtonFloatingActionButton widget 都有 onPressed() 回调,当用户点击 widget 时就会触发这些回调。

Many widgets use a GestureDetector to provide optional callbacks for other widgets. For example, the IconButton, ElevatedButton, and FloatingActionButton widgets have onPressed() callbacks that are triggered when the user taps the widget.

有关更多信息,请参阅 Flutter 中的手势

For more information, see Gestures in Flutter.

根据用户输入改变 widget

Changing widgets in response to input

到目前为止,这个页面仅使用了无状态的 widget。无状态 widget 接收的参数来自于它的父 widget,它们储存在 final 成员变量中。当 widget 需要被 build() 时,就是用这些存储的变量为创建的 widget 生成新的参数。

So far, this page has used only stateless widgets. Stateless widgets receive arguments from their parent widget, which they store in final member variables. When a widget is asked to build(), it uses these stored values to derive new arguments for the widgets it creates.

为了构建更复杂的体验,例如,以更有趣的方式对用户输入做出反应—应用通常带有一些状态。 Flutter 使用 StatefulWidgets 来实现这一想法。 StatefulWidgets 是一种特殊的 widget,它会生成 State 对象,用于保存状态。看看这个基本的例子,它使用了前面提到的 ElevatedButton

In order to build more complex experiences—for example, to react in more interesting ways to user input—applications typically carry some state. Flutter uses StatefulWidgets to capture this idea. StatefulWidgets are special widgets that know how to generate State objects, which are then used to hold state. Consider this basic example, using the ElevatedButton mentioned earlier:

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // This class is the configuration for the state.
  // It holds the values (in this case nothing) provided
  // by the parent and used by the build  method of the
  // State. Fields in a Widget subclass are always marked
  // "final".

  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework
      // that something has changed in this State, which
      // causes it to rerun the build method below so that
      // the display can reflect the updated values. If you
      // change _counter without calling setState(), then
      // the build method won't be called again, and so
      // nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called,
    // for instance, as done by the _increment method above.
    // The Flutter framework has been optimized to make
    // rerunning build methods fast, so that you can just
    // rebuild anything that needs updating rather than
    // having to individually changes instances of widgets.
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

您可能想知道为什么 StatefulWidget 和 State 是独立的对象。在 Flutter 中,这两种类型的对象具有不同的生命周期。 Widget 是临时对象,用于构造应用当前状态的展示。而 State 对象在调用 build() 之间是持久的,以此来存储信息。

You might wonder why StatefulWidget and State are separate objects. In Flutter, these two types of objects have different life cycles. Widgets are temporary objects, used to construct a presentation of the application in its current state. State objects, on the other hand, are persistent between calls to build(), allowing them to remember information.

上面的示例接受用户输入并直接在其 build() 方法中直接使用结果。在更复杂的应用中,widget 层次不同的部分可能负责不同的关注点;例如,一个 widget 可能呈现复杂的用户界面,来收集像日期或位置这样特定的信息,而另一个 widget 可能使用该信息来改变整体的展现。

The example above accepts user input and directly uses the result in its build() method. In more complex applications, different parts of the widget hierarchy might be responsible for different concerns; for example, one widget might present a complex user interface with the goal of gathering specific information, such as a date or location, while another widget might use that information to change the overall presentation.

在 Flutter 中,widget 通过回调得到状态改变的通知,同时当前状态通知给其他 widget 用于显示。重定向这一流程的共同父级是 State,下面稍微复杂的示例显示了它在实践中的工作原理:

In Flutter, change notifications flow “up” the widget hierarchy by way of callbacks, while current state flows “down” to the stateless widgets that do presentation. The common parent that redirects this flow is the State. The following slightly more complex example shows how this works in practice:

import 'package:flutter/material.dart';

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({required this.count, Key? key}) : super(key: key);

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  const CounterIncrementor({required this.onPressed, Key? key})
      : super(key: key);

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: const Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        CounterIncrementor(onPressed: _increment),
        const SizedBox(width: 16),
        CounterDisplay(count: _counter),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

注意创建两个新的无状态 widget 的方式,它清楚地分离了 显示 计数器(CounterDisplay)和 改变 计数器(CounterIncrementor)。尽管最终结果与前面的示例相同,但是责任的分离将更大的复杂性封装在各个 widget 中,保证了父级的简单性。

Notice the creation of two new stateless widgets, cleanly separating the concerns of displaying the counter (CounterDisplay) and changing the counter (CounterIncrementor). Although the net result is the same as the previous example, the separation of responsibility allows greater complexity to be encapsulated in the individual widgets, while maintaining simplicity in the parent.

有关更多信息,请参阅:

For more information, see:

整合在一起

Bringing it all together

下面是一个更完整的示例,汇集了上面介绍的概念:假定一个购物应用显示各种出售的产品,并在购物车中维护想购买的物品。首先定义一个用于展示的类,ShoppingListItem

What follows is a more complete example that brings together these concepts: A hypothetical shopping application displays various products offered for sale, and maintains a shopping cart for intended purchases. Start by defining the presentation class, ShoppingListItem:

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: ShoppingListItem(
            product: const Product(name: 'Chips'),
            inCart: true,
            onCartChanged: (product, inCart) {},
          ),
        ),
      ),
    ),
  );
}

ShoppingListItem widget 遵循无状态 widget 的通用模式。它将构造函数中接受到的值存储在 final 成员变量中,然后在 build() 函数中使用它们。例如,inCart 布尔值使两种样式进行切换:一个使用当前主题的主要颜色,另一个使用灰色。

The ShoppingListItem widget follows a common pattern for stateless widgets. It stores the values it receives in its constructor in final member variables, which it then uses during its build() function. For example, the inCart boolean toggles between two visual appearances: one that uses the primary color from the current theme, and another that uses gray.

当用户点击列表中的一项,widget 不会直接改变 inCart 的值,而是通过调用从父 widget 接收到的 onCartChanged 函数。这种方式可以在组件的生命周期中存储状态更长久,从而使状态持久化。甚至,widget 传给 runApp() 的状态可以持久到整个应用的生命周期。

When the user taps the list item, the widget doesn’t modify its inCart value directly. Instead, the widget calls the onCartChanged function it received from its parent widget. This pattern lets you store state higher in the widget hierarchy, which causes the state to persist for longer periods of time. In the extreme, the state stored on the widget passed to runApp() persists for the lifetime of the application.

当父级接收到 onCartChanged 回调时,父级会更新其内部状态,从而触发父级重建并使用新的 inCart 值来创建新的 ShoppingListItem 实例。尽管父级在重建时会创建 ShoppingListItem 的新实例,但是由于框架会将新构建的 widget 与先前构建的 widget 进行比较,仅将差异应用于底层的 RenderObject,这种代价是很小的。

When the parent receives the onCartChanged callback, the parent updates its internal state, which triggers the parent to rebuild and create a new instance of ShoppingListItem with the new inCart value. Although the parent creates a new instance of ShoppingListItem when it rebuilds, that operation is cheap because the framework compares the newly built widgets with the previously built widgets and applies only the differences to the underlying RenderObject.

这里有一个示例展示父组件是如何存储可变状态:

Here’s an example parent widget that stores mutable state:

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(
        product.name,
        style: _getTextStyle(context),
      ),
    );
  }
}

class ShoppingList extends StatefulWidget {
  const ShoppingList({required this.products, Key? key}) : super(key: key);

  final List<Product> products;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

  @override
  _ShoppingListState createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shopping List'),
      ),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: [
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

ShoppingList 类继承自 StatefulWidget,这意味着这个 widget 存储着可变状态。当 ShoppingList 首次插入到 widget 树中时,框架调用 createState() 函数来创建 _ShoppingListState 的新实例,以与树中的该位置相关联。(注意,State 的子类通常以下划线开头进行命名,表示它们的实现细节是私有的)当该 widget 的父 widget 重建时,父 widget 首先会创建一个 ShoppingList 的实例,但是框架会复用之前创建的 _ShoppingListState,而不会重新调用 createState

The ShoppingList class extends StatefulWidget, which means this widget stores mutable state. When the ShoppingList widget is first inserted into the tree, the framework calls the createState() function to create a fresh instance of _ShoppingListState to associate with that location in the tree. (Notice that subclasses of State are typically named with leading underscores to indicate that they are private implementation details.) When this widget’s parent rebuilds, the parent creates a new instance of ShoppingList, but the framework reuses the _ShoppingListState instance that is already in the tree rather than calling createState again.

为了访问当前 ShoppingList 的属性, _ShoppingListState 可以使用它的 widget 属性。当父组件重建一个新的 ShoppingList 时, _ShoppingListState 会使用新的 widget 值来创建。如果希望在 widget 属性更改时收到通知,则可以重写 didUpdateWidget() 函数,该函数将 oldWidget 作为参数传递,以便将 oldWidget 与当前 widget 进行比较。

To access properties of the current ShoppingList, the _ShoppingListState can use its widget property. If the parent rebuilds and creates a new ShoppingList, the _ShoppingListState rebuilds with the new widget value. If you wish to be notified when the widget property changes, override the didUpdateWidget() function, which is passed an oldWidget to let you compare the old widget with the current widget.

当处理 onCartChanged 回调时,_ShoppingListState 通过增加或删除 _shoppingCart 中的产品来改变其内部状态。为了通知框架它改变了它的内部状态,需要调用 setState(),将该 widget 标记为「dirty」(脏标记),并且计划在下次应用需要更新屏幕时重新构建它。如果在修改 widget 的内部状态后忘记调用 setState,框架将不知道这个 widget 是「dirty」(脏标记),并且可能不会调用 widget 的 build() 方法,这意味着用户界面可能不会更新以展示新的状态。通过以这种方式管理状态,你不需要编写用于创建和更新子 widget 的单独代码。相反,你只需实现 build 函数,它可以处理这两种情况。

When handling the onCartChanged callback, the _ShoppingListState mutates its internal state by either adding or removing a product from _shoppingCart. To signal to the framework that it changed its internal state, it wraps those calls in a setState() call. Calling setState marks this widget as dirty and schedules it to be rebuilt the next time your app needs to update the screen. If you forget to call setState when modifying the internal state of a widget, the framework won’t know your widget is dirty and might not call the widget’s build() function, which means the user interface might not update to reflect the changed state. By managing state in this way, you don’t need to write separate code for creating and updating child widgets. Instead, you simply implement the build function, which handles both situations.

响应 widget 的生命周期事件

Responding to widget lifecycle events

在 StatefulWidget 上调用 createState() 之后,框架将新的状态对象插入到树中,然后在状态对象上调用 initState()State 的子类可以重写 initState 来完成只需要发生一次的工作。例如,重写 initState 来配置动画或订阅平台服务。实现 initState 需要调用父类的 super.initState 方法来开始。

After calling createState() on the StatefulWidget, the framework inserts the new state object into the tree and then calls initState() on the state object. A subclass of State can override initState to do work that needs to happen just once. For example, override initState to configure animations or to subscribe to platform services. Implementations of initState are required to start by calling super.initState.

当不再需要状态对象时,框架会调用状态对象上的 dispose() 方法。可以重写dispose 方法来清理状态。例如,重写 dispose 以取消计时器或取消订阅平台服务。实现 dispose 时通常通过调用 super.dispose 来结束。

When a state object is no longer needed, the framework calls dispose() on the state object. Override the dispose function to do cleanup work. For example, override dispose to cancel timers or to unsubscribe from platform services. Implementations of dispose typically end by calling super.dispose.

有关更多信息,请参阅 State

For more information, see State.

Keys

使用 key 可以控制框架在 widget 重建时与哪些其他 widget 进行匹配。默认情况下,框架根据它们的 runtimeType 以及它们的显示顺序来匹配。使用 key 时,框架要求两个 widget 具有相同的 keyruntimeType

Use keys to control which widgets the framework matches up with other widgets when a widget rebuilds. By default, the framework matches widgets in the current and previous build according to their runtimeType and the order in which they appear. With keys, the framework requires that the two widgets have the same key as well as the same runtimeType.

Key 在构建相同类型 widget 的多个实例时很有用。例如,ShoppingList widget,它只构建刚刚好足够的 ShoppingListItem 实例来填充其可见区域:

Keys are most useful in widgets that build many instances of the same type of widget. For example, the ShoppingList widget, which builds just enough ShoppingListItem instances to fill its visible region:

有关更多信息,请参阅 Key API。

For more information, see the Key API.

全局 key

Global keys

全局 key 可以用来标识唯一子 widget。全局 key 在整个 widget 结构中必须是全局唯一的,而不像本地 key 只需要在兄弟 widget 中唯一。由于它们是全局唯一的,因此可以使用全局 key 来检索与 widget 关联的状态。

Use global keys to uniquely identify child widgets. Global keys must be globally unique across the entire widget hierarchy, unlike local keys which need only be unique among siblings. Because they are globally unique, a global key can be used to retrieve the state associated with a widget.

有关更多信息,请参阅 GlobalKey API。

For more information, see the GlobalKey API.

Flutter 中的布局

目录

Flutter 布局的核心机制是 widgets。在 Flutter 中,几乎所有东西都是 widget —— 甚至布局模型都是 widgets。你在 Flutter 应用程序中看到的图像,图标和文本都是 widgets。此外不能直接看到的也是 widgets,例如用来排列、限制和对齐可见 widgets 的行、列和网格。

The core of Flutter’s layout mechanism is widgets. In Flutter, almost everything is a widget—even layout models are widgets. The images, icons, and text that you see in a Flutter app are all widgets. But things you don’t see are also widgets, such as the rows, columns, and grids that arrange, constrain, and align the visible widgets.

你可以通过组合 widgets 来构建更复杂的 widgets 来创建布局。比如,下面第一个截图上有 3 个图标,每个图标下面都有一个标签:

You create a layout by composing widgets to build more complex widgets. For example, the first screenshot below shows 3 icons with a label under each one:

Sample layout Sample layout with visual debugging

第二个截图显示了可视布局,可以看到有一排三列,其中每列包含一个图标和一个标签。

The second screenshot displays the visual layout, showing a row of 3 columns where each column contains an icon and a label.

以下是这个 UI 的 widget 树形图:

Here’s a diagram of the widget tree for this UI:

Node tree

图上大部分应该和你预想的一样,但你可能会疑惑 containers(图上粉色显示的)是什么。 Container 是一个 widget,允许你自定义其子 widget。举几个例子,如果要添加 padding、margin、边框或背景颜色,你就可以用上 Container 了。

Most of this should look as you might expect, but you might be wondering about the containers (shown in pink). Container is a widget class that allows you to customize its child widget. Use a Container when you want to add padding, margins, borders, or background color, to name some of its capabilities.

在这个例子中,每个 Text widget 都被放在一个 Container 以添加 padding。整个 Row 也被放在一个 Container 中,以便添加 padding。

In this example, each Text widget is placed in a Container to add margins. The entire Row is also placed in a Container to add padding around the row.

这个例子其余部分的 UI 由属性控制。通过 Iconcolor 属性来设置它的颜色,通过 Text.style 属性来设置文字的字体、颜色、字重等等。列和行有一些属性可以让你指定子项垂直或水平的对齐方式以及子项应占用的空间大小。

The rest of the UI in this example is controlled by properties. Set an Icon’s color using its color property. Use the Text.style property to set the font, its color, weight, and so on. Columns and rows have properties that allow you to specify how their children are aligned vertically or horizontally, and how much space the children should occupy.

布局 widget

Lay out a widget

如何在 Flutter 中布局单个 widget?本节将介绍如何创建和显示单个 widget。本节还包括一个简单的 Hello World app 的完整代码。

How do you lay out a single widget in Flutter? This section shows you how to create and display a simple widget. It also shows the entire code for a simple Hello World app.

在 Flutter 中,只需几步就可以在屏幕上显示文本、图标或图像。

In Flutter, it takes only a few steps to put text, an icon, or an image on the screen.

1. 选择一个布局 widget

1. Select a layout widget

根据你想要对齐或限制可见 widget 的方式从各种 layout widgets 中进行选择,因为这些特性通常会传递它所给包含的 widget。

Choose from a variety of layout widgets based on how you want to align or constrain the visible widget, as these characteristics are typically passed on to the contained widget.

本例使用将其内容水平和垂直居中的 Center

This example uses Center which centers its content horizontally and vertically.

2. 创建一个可见 widget

2. Create a visible widget

举个例子,创建一个 Text widget:

For example, create a Text widget:

Text('Hello World'),

创建一个 Image widget:

Create an Image widget:

Image.asset(
  'images/lake.jpg',
  fit: BoxFit.cover,
),

创建一个 Icon widget:

Create an Icon widget:

Icon(
  Icons.star,
  color: Colors.red[500],
),

3. 将可见 widget 添加到布局 widget

3. Add the visible widget to the layout widget

所有布局 widgets 都具有以下任一项:

All layout widgets have either of the following:

Text widget 添加进 Center widget:

Add the Text widget to the Center widget:

const Center(
  child: Text('Hello World'),
),

4. 将布局 widget 添加到页面

4. Add the layout widget to the page

一个 Flutter app 本身就是一个 widget,大多数 widgets 都有一个 build() 方法,在 app 的 build() 方法中实例化和返回一个 widget 会让它显示出来。

A Flutter app is itself a widget, and most widgets have a build() method. Instantiating and returning a widget in the app’s build() method displays the widget.

对于 Material app,你可以使用 Scaffold widget,它提供默认的 banner 背景颜色,还有用于添加抽屉、提示条和底部列表弹窗的 API。你可以将 Center widget 直接添加到主页 body 的属性中。

For a Material app, you can use a Scaffold widget; it provides a default banner, background color, and has API for adding drawers, snack bars, and bottom sheets. Then you can add the Center widget directly to the body property for the home page.

lib/main.dart (MyApp)
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter layout demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout demo'),
        ),
        body: const Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

非 Material apps

Non-Material apps

对于非 Material app,你可以将 Center widget 添加到 app 的 build() 方法里:

For a non-Material app, you can add the Center widget to the app’s build() method:

lib/main.dart (MyApp)
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(color: Colors.white),
      child: const Center(
        child: Text(
          'Hello World',
          textDirection: TextDirection.ltr,
          style: TextStyle(
            fontSize: 32,
            color: Colors.black87,
          ),
        ),
      ),
    );
  }
}

默认情况下,非 Material app 不包含 AppBar、标题和背景颜色。如果你希望在非 Material app 中使用这些功能,则必须自己构建它们。以上 app 将背景颜色更改为白色,将文本更改为深灰色来模拟一个 Material app。

By default a non-Material app doesn’t include an AppBar, title, or background color. If you want these features in a non-Material app, you have to build them yourself. This app changes the background color to white and the text to dark grey to mimic a Material app.

完成! 启动这个 app,你应该能看到 Hello World

That’s it! When you run the app, you should see Hello World.

App 源码:

App source code:

Hello World

横向或纵向布局多个 widgets

Lay out multiple widgets vertically and horizontally

最常见的布局模式之一是垂直或水平 widgets。你可以使用 Row widget 水平排列 widgets,使用 Column widget 垂直排列 widgets。

One of the most common layout patterns is to arrange widgets vertically or horizontally. You can use a Row widget to arrange widgets horizontally, and a Column widget to arrange widgets vertically.

要在 Flutter 中创建行或列,可以将子 widgets 列表添加到 RowColumn widget 中。反过来,每个子项本身可以是一行或一列,依此类推。以下示例演示了如何在行或列中嵌套行或列。

To create a row or column in Flutter, you add a list of children widgets to a Row or Column widget. In turn, each child can itself be a row or column, and so on. The following example shows how it is possible to nest rows or columns inside of rows or columns.

这个布局被组织为 Row。这一行包含两个子项:左侧的列和右侧的图像:

This layout is organized as a Row. The row contains two children: a column on the left, and an image on the right:

Screenshot with callouts showing the row containing two children

左侧列的 widget 树嵌套着行和列。

The left column’s widget tree nests rows and columns.

Diagram showing a left column broken down to its sub-rows and sub-columns

你将在 嵌套行和列 中实现蛋糕介绍示例的一些布局代码。

You’ll implement some of Pavlova’s layout code in Nesting rows and columns.

对齐 widgets

Aligning widgets

你可以使用 mainAxisAlignmentcrossAxisAlignment 属性控制行或列如何对齐其子项。对于一行来说,主轴水平延伸,交叉轴垂直延伸。对于一列来说,主轴垂直延伸,交叉轴水平延伸。

You control how a row or column aligns its children using the mainAxisAlignment and crossAxisAlignment properties. For a row, the main axis runs horizontally and the cross axis runs vertically. For a column, the main axis runs vertically and the cross axis runs horizontally.

Diagram showing the main axis and cross axis for a row Diagram showing the main axis and cross axis for a column

MainAxisAlignmentCrossAxisAlignment 这两个类提供了很多用于控制对齐的常量。

The MainAxisAlignment and CrossAxisAlignment classes offer a variety of constants for controlling alignment.

在以下示例中,3 个图像每个都是是 100 像素宽。渲染框(在本例中是整个屏幕)宽度超过 300 像素,因此设置主轴对齐方式为 spaceEvenly 会将空余空间在每个图像之间、之前和之后均匀地划分。

In the following example, each of the 3 images is 100 pixels wide. The render box (in this case, the entire screen) is more than 300 pixels wide, so setting the main axis alignment to spaceEvenly divides the free horizontal space evenly between, before, and after each image.

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Image.asset('images/pic1.jpg'),
    Image.asset('images/pic2.jpg'),
    Image.asset('images/pic3.jpg'),
  ],
);

Row with 3 evenly spaced images

App 源码: row_column

App source: row_column

列的工作方式与行的工作方式相同。以下示例展示了包含 3 个图像的列,每个图像的高度为 100 像素。渲染框(在本例中是整个屏幕)高度超过 300 像素,因此设置主轴对齐方式为 spaceEvenly 会将空余空间在每个图像之间、之上和之下均匀地划分。

Columns work the same way as rows. The following example shows a column of 3 images, each is 100 pixels high. The height of the render box (in this case, the entire screen) is more than 300 pixels, so setting the main axis alignment to spaceEvenly divides the free vertical space evenly between, above, and below each image.

Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Image.asset('images/pic1.jpg'),
    Image.asset('images/pic2.jpg'),
    Image.asset('images/pic3.jpg'),
  ],
);

App 源码: row_column

App source: row_column

Column showing 3 images spaced evenly

调整 widgets 大小

Sizing widgets

当某个布局太大而超出屏幕时,受影响的边缘会出现黄色和黑色条纹图案。这里有一个行太宽的 例子

When a layout is too large to fit a device, a yellow and black striped pattern appears along the affected edge. Here is an example of a row that is too wide:

Overly-wide row

通过使用 Expanded widget,可以调整 widgets 的大小以适合行或列。要修复上一个图像行对其渲染框来说太宽的示例,可以用 Expanded widget 把每个图像包起来。

Widgets can be sized to fit within a row or column by using the Expanded widget. To fix the previous example where the row of images is too wide for its render box, wrap each image with an Expanded widget.

Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Expanded(
      child: Image.asset('images/pic1.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic2.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic3.jpg'),
    ),
  ],
);

Row of 3 images that are too wide, but each is constrained to take only 1/3 of the space

App 源码: sizing

App source: sizing

也许你想要一个 widget 占用的空间是兄弟项的两倍。为了达到这个效果,可以使用 Expanded widget 的 flex 属性,这是一个用来确定 widget 的弹性系数的整数。默认的弹性系数为 1,以下代码将中间图像的弹性系数设置为 2:

Perhaps you want a widget to occupy twice as much space as its siblings. For this, use the Expanded widget flex property, an integer that determines the flex factor for a widget. The default flex factor is 1. The following code sets the flex factor of the middle image to 2:

Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Expanded(
      child: Image.asset('images/pic1.jpg'),
    ),
    Expanded(
      flex: 2,
      child: Image.asset('images/pic2.jpg'),
    ),
    Expanded(
      child: Image.asset('images/pic3.jpg'),
    ),
  ],
);

Row of 3 images with the middle image twice as wide as the others

App 源码: sizing

App source: sizing

组合 widgets

Packing widgets

默认情况下,行或列沿其主轴会占用尽可能多的空间,但如果要将子项紧密组合在一起,请将其 mainAxisSize 设置为 MainAxisSize.min。以下示例使用此属性将星形图标组合在一起。

By default, a row or column occupies as much space along its main axis as possible, but if you want to pack the children closely together, set its mainAxisSize to MainAxisSize.min. The following example uses this property to pack the star icons together.

Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    const Icon(Icons.star, color: Colors.black),
    const Icon(Icons.star, color: Colors.black),
  ],
)

Row of 5 stars, packed together in the middle of the row

App 源码: pavlova

App source: pavlova

嵌套行和列

Nesting rows and columns

布局框架允许你根据需要在行和列内嵌套行和列。让我们看看以下布局的概述部分的代码:

The layout framework allows you to nest rows and columns inside of rows and columns as deeply as you need. Let’s look at the code for the outlined section of the following layout:

Screenshot of the pavlova app, with the ratings and icon rows outlined in red

概述的部分实现为两行,评级一行包含五颗星和评论的数量,图标一行包含由图标与文本组成的三列。

The outlined section is implemented as two rows. The ratings row contains five stars and the number of reviews. The icons row contains three columns of icons and text.

以下是评级行的 widget 树形图:

The widget tree for the ratings row:

Ratings row widget tree

ratings 变量创建了一个行,其中包含较小的由 5 个星形图标和文本组成的一行:

The ratings variable creates a row containing a smaller row of 5 star icons, and text:

var stars = Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    Icon(Icons.star, color: Colors.green[500]),
    const Icon(Icons.star, color: Colors.black),
    const Icon(Icons.star, color: Colors.black),
  ],
);

final ratings = Container(
  padding: const EdgeInsets.all(20),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      stars,
      const Text(
        '170 Reviews',
        style: TextStyle(
          color: Colors.black,
          fontWeight: FontWeight.w800,
          fontFamily: 'Roboto',
          letterSpacing: 0.5,
          fontSize: 20,
        ),
      ),
    ],
  ),
);

评级行下方的图标行包含 3 列,每列包含一个图标和两行文本,你可以在其 widget 树中看到:

The icons row, below the ratings row, contains 3 columns; each column contains an icon and two lines of text, as you can see in its widget tree:

Icon widget tree

iconList 变量定义了图标行:

The iconList variable defines the icons row:

const descTextStyle = TextStyle(
  color: Colors.black,
  fontWeight: FontWeight.w800,
  fontFamily: 'Roboto',
  letterSpacing: 0.5,
  fontSize: 18,
  height: 2,
);

// DefaultTextStyle.merge() allows you to create a default text
// style that is inherited by its child and all subsequent children.
final iconList = DefaultTextStyle.merge(
  style: descTextStyle,
  child: Container(
    padding: const EdgeInsets.all(20),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Column(
          children: [
            Icon(Icons.kitchen, color: Colors.green[500]),
            const Text('PREP:'),
            const Text('25 min'),
          ],
        ),
        Column(
          children: [
            Icon(Icons.timer, color: Colors.green[500]),
            const Text('COOK:'),
            const Text('1 hr'),
          ],
        ),
        Column(
          children: [
            Icon(Icons.restaurant, color: Colors.green[500]),
            const Text('FEEDS:'),
            const Text('4-6'),
          ],
        ),
      ],
    ),
  ),
);

leftColumn 变量包含评级和图标行,以及蛋糕介绍的标题和文本:

The leftColumn variable contains the ratings and icons rows, as well as the title and text that describes the Pavlova:

final leftColumn = Container(
  padding: const EdgeInsets.fromLTRB(20, 30, 20, 20),
  child: Column(
    children: [
      titleText,
      subTitle,
      ratings,
      iconList,
    ],
  ),
);

左列放置在 Container 中以限制其宽度。最后,UI 由 Card 内的整行(包含左列和图像)构成。

The left column is placed in a SizedBox to constrain its width. Finally, the UI is constructed with the entire row (containing the left column and the image) inside a Card.

蛋糕图片 来自 Pixabay 网站。你可以使用 Image.network() 从网络上引用图像,但是在本例图像将保存到项目中的一个图像目录中,添加到 pubspec 文件,并使用 Images.asset() 访问。更多信息可以查看文档中关于 添加资源和图片 这一章。

The Pavlova image is from Pixabay. You can embed an image from the net using Image.network() but, for this example, the image is saved to an images directory in the project, added to the pubspec file, and accessed using Images.asset(). For more information, see Adding assets and images.

body: Center(
  child: Container(
    margin: const EdgeInsets.fromLTRB(0, 40, 0, 30),
    height: 600,
    child: Card(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 440,
            child: leftColumn,
          ),
          mainImage,
        ],
      ),
    ),
  ),
),

App 源码: pavlova

App source: pavlova


通用布局 widgets

Common layout widgets

Flutter 有一个丰富的布局 widget 仓库,里面有很多经常会用到的布局 widget。目的是为了让你更快的上手,而不是被一个完整的列表吓跑。关于其他有用的 widget 的信息,可以参考 Widget 目录,或者使用 API 参考文档 中的搜索框。而且,API 文档中的 widget 页面中经常会给出一些关于相似的 widget 哪个会更适合你的建议。

Flutter has a rich library of layout widgets. Here are a few of those most commonly used. The intent is to get you up and running as quickly as possible, rather than overwhelm you with a complete list. For information on other available widgets, refer to the Widget catalog, or use the Search box in the API reference docs. Also, the widget pages in the API docs often make suggestions about similar widgets that might better suit your needs.

下面的 widget 会分为两类:widgets 库 中的标准 widgets 和 Material 库 中的 widgets。任何 app 都可以使用 widget 库,但是 Material 库中的组件只能在 Material app 中使用。

The following widgets fall into two categories: standard widgets from the widgets library, and specialized widgets from the Material library. Any app can use the widgets library but only Material apps can use the Material Components library.

标准 widgets

Standard widgets

Material widgets

Container

许多布局都可以随意的用 Container,它可以将使用了 padding 或者增加了 borders/margins 的 widget 分开。你可以通过将整个布局放到一个 Container 中,并且改变它的背景色或者图片,来改变设备的背景。

Many layouts make liberal use of Containers to separate widgets using padding, or to add borders or margins. You can change the device’s background by placing the entire layout into a Container and changing its background color or image.

摘要 (Container)

Summary (Container)

  • 增加 padding、margins、borders

    Add padding, margins, borders

  • 改变背景色或者图片

    Change background color or image

  • 只包含一个子 widget,但是这个子 widget 可以是行、列或者是 widget 树的根 widget

    Contains a single child widget, but that child can be a Row, Column, or even the root of a widget tree

Diagram showing: margin, border, padding, and content

示例 (Container)

Examples (Container)

这个布局包含一个有两行的列,每行有两张图片。 Container 用来将列的背景色变为浅灰色。

This layout consists of a column with two rows, each containing 2 images. A Container is used to change the background color of the column to a lighter grey.

Widget _buildImageColumn() {
  return Container(
    decoration: const BoxDecoration(
      color: Colors.black26,
    ),
    child: Column(
      children: [
        _buildImageRow(1),
        _buildImageRow(3),
      ],
    ),
  );
}
Screenshot showing 2 rows, each containing 2 images

Container 还用来为每个图片添加圆角和外边距:

A Container is also used to add a rounded border and margins to each image:

Widget _buildDecoratedImage(int imageIndex) => Expanded(
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(width: 10, color: Colors.black38),
          borderRadius: const BorderRadius.all(Radius.circular(8)),
        ),
        margin: const EdgeInsets.all(4),
        child: Image.asset('images/pic$imageIndex.jpg'),
      ),
    );

Widget _buildImageRow(int imageIndex) => Row(
      children: [
        _buildDecoratedImage(imageIndex),
        _buildDecoratedImage(imageIndex + 1),
      ],
    );

你可以在 布局构建教程Flutter Gallery 中可以发现更多关于 Container 的例子。

You can find more Container examples in the tutorial and the Flutter Gallery (running app, repo).

App 源码: container

App source: container


GridView

使用 GridView 将 widget 作为二维列表展示。 GridView 提供两个预制的列表,或者你可以自定义网格。当 GridView 检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。

Use GridView to lay widgets out as a two-dimensional list. GridView provides two pre-fabricated lists, or you can build your own custom grid. When a GridView detects that its contents are too long to fit the render box, it automatically scrolls.

摘要 (GridView)

Summary (GridView)

示例 (GridView)

Examples (GridView)

A 3-column grid of photos

使用 GridView.extent 创建一个最大宽度为 150 像素的网格。

Uses GridView.extent to create a grid with tiles a maximum 150 pixels wide.

App 源码: grid_and_list

App source: grid_and_list

A 2 column grid with footers

使用 GridView.count 创建一个网格,它在竖屏模式下有两行,在横屏模式下有三行。可以通过为每个 GridTile 设置 footer 属性来创建标题。

Uses GridView.count to create a grid that’s 2 tiles wide in portrait mode, and 3 tiles wide in landscape mode. The titles are created by setting the footer property for each GridTile.

Dart 代码: Flutter Gallery 中的 grid_list_demo.dart

Dart code: grid_list_demo.dart from the Flutter Gallery

Widget _buildGrid() => GridView.extent(
    maxCrossAxisExtent: 150,
    padding: const EdgeInsets.all(4),
    mainAxisSpacing: 4,
    crossAxisSpacing: 4,
    children: _buildGridTileList(30));

// The images are saved with names pic0.jpg, pic1.jpg...pic29.jpg.
// The List.generate() constructor allows an easy way to create
// a list when objects have a predictable naming pattern.
List<Container> _buildGridTileList(int count) => List.generate(
    count, (i) => Container(child: Image.asset('images/pic$i.jpg')));

ListView

ListView,一个和列很相似的 widget,当内容长于自己的渲染盒时,就会自动支持滚动。

ListView, a column-like widget, automatically provides scrolling when its content is too long for its render box.

摘要 (ListView)

Summary (ListView)

示例 (ListView)

Examples (ListView)

ListView containing movie theaters and restaurants

使用 ListView 的业务列表,它使用了多个 ListTileDivider 将餐厅从剧院中分隔开。

Uses ListView to display a list of businesses using ListTiles. A Divider separates the theaters from the restaurants.

App 源码: grid_and_list

App source: grid_and_list

ListView containing shades of blue

使用 ListView 展示特定颜色系列 Material Design 调色板 中的 Colors

Uses ListView to display the Colors from the Material Design palette for a particular color family.

Dart 代码: Flutter Gallery 中的 colors_demo.dart

Dart code: colors_demo.dart from the Flutter Gallery

Widget _buildList() {
  return ListView(
    children: [
      _tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),
      _tile('The Castro Theater', '429 Castro St', Icons.theaters),
      _tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),
      _tile('Roxie Theater', '3117 16th St', Icons.theaters),
      _tile('United Artists Stonestown Twin', '501 Buckingham Way',
          Icons.theaters),
      _tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),
      const Divider(),
      _tile('K\'s Kitchen', '757 Monterey Blvd', Icons.restaurant),
      _tile('Emmy\'s Restaurant', '1923 Ocean Ave', Icons.restaurant),
      _tile(
          'Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),
      _tile('La Ciccia', '291 30th St', Icons.restaurant),
    ],
  );
}

ListTile _tile(String title, String subtitle, IconData icon) {
  return ListTile(
    title: Text(title,
        style: const TextStyle(
          fontWeight: FontWeight.w500,
          fontSize: 20,
        )),
    subtitle: Text(subtitle),
    leading: Icon(
      icon,
      color: Colors.blue[500],
    ),
  );
}

Stack

可以使用 Stack 在基础 widget(通常是图片)上排列 widget, widget 可以完全或者部分覆盖基础 widget。

Use Stack to arrange widgets on top of a base widget—often an image. The widgets can completely or partially overlap the base widget.

摘要 (Stack)

Summary (Stack)

示例 (Stack)

Examples (Stack)

Circular avatar image with a label

CircleAvatar 的上面使用 Stack 覆盖 Container (在透明的黑色背景上展示它的 Text)。 Stack 使用 alignment 属性和 Alignment 让文本偏移。

Uses Stack to overlay a Container (that displays its Text on a translucent black background) on top of a CircleAvatar. The Stack offsets the text using the alignment property and Alignments.

App 源码: card_and_stack

App source: card_and_stack

An image with a grey gradient across the top

使用 Stack 将渐变叠加到图片的顶部,渐变可以将工具栏的图标和图片区分开来。

Uses Stack to overlay a gradient to the top of the image. The gradient ensures that the toolbar’s icons are distinct against the image.

Dart 代码: Flutter Gallery 中的 contacts_demo.dart

Dart code: contacts_demo.dart from the Flutter Gallery

Widget _buildStack() {
  return Stack(
    alignment: const Alignment(0.6, 0.6),
    children: [
      const CircleAvatar(
        backgroundImage: AssetImage('images/pic.jpg'),
        radius: 100,
      ),
      Container(
        decoration: const BoxDecoration(
          color: Colors.black45,
        ),
        child: const Text(
          'Mia B',
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
    ],
  );
}

Card

Material 库 中的 Card 包含相关有价值的信息,几乎可以由任何 widget 组成,但是通常和 ListTile 一起使用。 Card 只有一个子项,这个子项可以是列、行、列表、网格或者其他支持多个子项的 widget。默认情况下,Card 的大小是 0x0 像素。你可以使用 SizedBox 控制 card 的大小。

A Card, from the Material library, contains related nuggets of information and can be composed from almost any widget, but is often used with ListTile. Card has a single child, but its child can be a column, row, list, grid, or other widget that supports multiple children. By default, a Card shrinks its size to 0 by 0 pixels. You can use SizedBox to constrain the size of a card.

在 Flutter 中,Card 有轻微的圆角和阴影来使它具有 3D 效果。改变 Cardelevation 属性可以控制阴影效果。例如,把 elevation 设置为 24,可以从视觉上更多的把 Card 抬离表面,使阴影变得更加分散。关于支持的 elevation 的值的列表,可以查看 Material guidelines 中的 Elevation。使用不支持的值则会使阴影无效。

In Flutter, a Card features slightly rounded corners and a drop shadow, giving it a 3D effect. Changing a Card’s elevation property allows you to control the drop shadow effect. Setting the elevation to 24, for example, visually lifts the Card further from the surface and causes the shadow to become more dispersed. For a list of supported elevation values, see Elevation in the Material guidelines. Specifying an unsupported value disables the drop shadow entirely.

摘要 (Card)

Summary (Card)

示例 (Card)

Examples (Card)

Card containing 3 ListTiles

包含 3 个 ListTile 的 Card,并且通过被 SizedBox 包住来调整大小。 Divider 分隔了第一个和第二个 ListTiles

A Card containing 3 ListTiles and sized by wrapping it with a SizedBox. A Divider separates the first and second ListTiles.

App 源码: card_and_stack

App source: card_and_stack

Card containing an image, text and buttons

包含图片和文本的 Card

A Card containing an image and text.

Dart 代码: Flutter Gallery 中的 cards_demo.dart

Dart code: cards_demo.dart from the Flutter Gallery

Widget _buildCard() {
  return SizedBox(
    height: 210,
    child: Card(
      child: Column(
        children: [
          ListTile(
            title: const Text(
              '1625 Main Street',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            subtitle: const Text('My City, CA 99984'),
            leading: Icon(
              Icons.restaurant_menu,
              color: Colors.blue[500],
            ),
          ),
          const Divider(),
          ListTile(
            title: const Text(
              '(408) 555-1212',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            leading: Icon(
              Icons.contact_phone,
              color: Colors.blue[500],
            ),
          ),
          ListTile(
            title: const Text('costa@example.com'),
            leading: Icon(
              Icons.contact_mail,
              color: Colors.blue[500],
            ),
          ),
        ],
      ),
    ),
  );
}

ListTile

ListTileMaterial 库 中专用的行 widget,它可以很轻松的创建一个包含三行文本以及可选的行前和行尾图标的行。 ListTileCard 或者 ListView 中最常用,但是也可以在别处使用。

Use ListTile, a specialized row widget from the Material library, for an easy way to create a row containing up to 3 lines of text and optional leading and trailing icons. ListTile is most commonly used in Card or ListView, but can be used elsewhere.

摘要 (ListTile)

Summary (ListTile)

示例 (ListTile)

Examples (ListTile)

Card containing 3 ListTiles

包含 3 个 ListTilesCard

A Card containing 3 ListTiles.

App 源码: card_and_stack

App source: card_and_stack

3 ListTiles, each containing a pull-down button

使用 ListTile 列出 3 个下拉按钮类型。

Uses ListTile to list 3 drop down button types.

Dart 代码: Flutter Gallery 中的 buttons_demo.dart

Dart code: buttons_demo.dart from the Flutter Gallery


Constraints

To fully understand Flutter’s layout system, you need to learn how Flutter positions and sizes the components in a layout. For more information, see Understanding constraints.

视频

Videos

下面的视频是 Flutter in Focus 系列的一部分,解释了 Stateless 和 Stateful 的 widget。

The following videos, part of the Flutter in Focus series, explain Stateless and Stateful widgets.

Flutter in Focus playlist


每周 Widget 系列 的每一集都会介绍一个 widget。其中也包括一些布局的 widget。

Each episode of the Widget of the Week series focuses on a widget. Several of them includes layout widgets.

Flutter Widget of the Week playlist

其他资源

Other resources

当写布局代码时,下面的资源可能会帮助到你。

The following resources might help when writing layout code.

布局构建教程

目录

这是一份如何在 Flutter 中构建布局的指南。你将为如下 app 创建布局:

This is a guide to building layouts in Flutter. You’ll build the layout for the following app:

The finished app
The finished app

这份指南之前溯源一步解释了 Flutter 中的布局方式,以及展示了如何在屏幕中放置单个 widget。经过了如何水平以及竖直放置 widgets 的讨论之后,一些最常使用的 widgets 都涉及到了。

This guide then takes a step back to explain Flutter’s approach to layout, and shows how to place a single widget on the screen. After a discussion of how to lay widgets out horizontally and vertically, some of the most common layout widgets are covered.

如果你想对布局机制有个”全局”的理解,可以先从 Flutter 中的布局 开始.

If you want a “big picture” understanding of the layout mechanism, start with Flutter’s approach to layout.

第一步: 创建 app 基础代码

Step 0: Create the app base code

确保你已经 安装和配置 好了你的环境,然后做如下步骤:

Make sure to set up your environment, then do the following:

  1. 创建一个简单的 Flutter app ——”Hello World”

    Create a basic “Hello World” Flutter app.

  2. 按照如下方法修改 app 标题栏的标题以及 app 的标题:

    Change the app bar title and the app title as follows:

    layout/base/lib/{main_starter.dart → main.dart}
    @@ -12,10 +12,10 @@
    12
    12
      @override
    13
    13
      Widget build(BuildContext context) {
    14
    14
      return MaterialApp(
    15
    - title: 'Welcome to Flutter',
    15
    + title: 'Flutter layout demo',
    16
    16
      home: Scaffold(
    17
    17
      appBar: AppBar(
    18
    - title: const Text('Welcome to Flutter'),
    18
    + title: const Text('Flutter layout demo'),
    19
    19
      ),
    20
    20
      body: const Center(
    21
    21
      child: Text('Hello World'),

第一步: 对布局进行图形分解

Step 1: Diagram the layout

第一步需要将布局分解成它的各个基础元素:

The first step is to break the layout down to its basic elements:

首先,识别出稍大的元素。在这个例子中,四个元素排成一列:一个图像,两个行区域,和一个文本区域。

First, identify the larger elements. In this example, four elements are arranged into a column: an image, two rows, and a block of text.

Column elements (circled in red)
Column elements (circled in red)

接着,对每一行进行图解。第一行,也就是标题区域,有三个子元素:一个文本列,一个星形图标,和一个数字。它的第一个子元素,文本列,包含两行文本。第一列占据大量空间,因此它应当被封装在一个 Expanded widget 当中。

Next, diagram each row. The first row, called the Title section, has 3 children: a column of text, a star icon, and a number. Its first child, the column, contains 2 lines of text. That first column takes a lot of space, so it must be wrapped in an Expanded widget.

Title section

第二行,也就是按钮区域,同样有三个子元素:每个子元素是一个包含图标和文本的列。

The second row, called the Button section, also has 3 children: each child is a column that contains an icon and text.

Button section

一旦图解好布局,采取自下而上的方法来实现它就变得尤为轻松了。为了最大程度减少,深层嵌套的布局代码带来的视觉混乱,需要用一些变量和函数来替代某些实现。

Once the layout has been diagrammed, it’s easiest to take a bottom-up approach to implementing it. To minimize the visual confusion of deeply nested layout code, place some of the implementation in variables and functions.

第二步: 实现标题行

Step 2: Implement the title row

首先,你可以构建标题部分左侧列。添加如下代码到 MyApp 类的 build() 方法内顶部。

First, you’ll build the left column in the title section. Add the following code at the top of the build() method of the MyApp class:

lib/main.dart (titleSection)
Widget titleSection = Container(
  padding: const EdgeInsets.all(32),
  child: Row(
    children: [
      Expanded(
        /*1*/
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            /*2*/
            Container(
              padding: const EdgeInsets.only(bottom: 8),
              child: const Text(
                'Oeschinen Lake Campground',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            Text(
              'Kandersteg, Switzerland',
              style: TextStyle(
                color: Colors.grey[500],
              ),
            ),
          ],
        ),
      ),
      /*3*/
      Icon(
        Icons.star,
        color: Colors.red[500],
      ),
      const Text('41'),
    ],
  ),
);
  1. 将 Column 元素放到 Expanded widget 中可以拉伸该列,以利用该行中所有剩余的闲置空间。设置 crossAxisAlignment 属性值为 CrossAxisAlignment.start,这会将该列放置在行的起始位置。

    Putting a Column inside an Expanded widget stretches the column to use all remaining free space in the row. Setting the crossAxisAlignment property to CrossAxisAlignment.start positions the column at the start of the row.

  2. 将第一行文本放入 Container 容器中使得你可以增加内间距。列中的第二个子元素,同样为文本,显示为灰色。

    Putting the first row of text inside a Container enables you to add padding. The second child in the Column, also text, displays as grey.

  3. 标题行中的最后两项是一个红色星形图标,和文字”41”。整行都在一个 Container 容器布局中,而且每条边都有 32 像素的内间距。

    The last two items in the title row are a star icon, painted red, and the text “41”. The entire row is in a Container and padded along each edge by 32 pixels. Add the title section to the app body like this:

如下添加标题部分到 app body 中:

Add the title section to the app body like this:

{../base → step2}/lib/main.dart
@@ -14,11 +48,13 @@
14
48
  return MaterialApp(
15
49
  title: 'Flutter layout demo',
16
50
  home: Scaffold(
17
51
  appBar: AppBar(
18
52
  title: const Text('Flutter layout demo'),
19
53
  ),
20
- body: const Center(
21
- child: Text('Hello World'),
54
+ body: Column(
55
+ children: [
56
+ titleSection,
57
+ ],
22
58
  ),
23
59
  ),
24
60
  );

第三步: 实现按钮行

Step 3: Implement the button row

按钮区域包含三列使用相同布局-一行文本上面一个图标。此行的各列被等间隙放置,文本和图标被着以初始色。

The button section contains 3 columns that use the same layout—an icon over a row of text. The columns in this row are evenly spaced, and the text and icons are painted with the primary color.

由于构建每列的代码基本相同,因此可以创建一个名为 buildButtonColumn() 的私有辅助函数,以颜色、图标和文本为入参,返回一个以指定颜色绘制自身 widgets 的一个 column 列对象。

Since the code for building each column is almost identical, create a private helper method named buildButtonColumn(), which takes a color, an Icon and Text, and returns a column with its widgets painted in the given color.

lib/main.dart (_buildButtonColumn)
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ···
  }

  Column _buildButtonColumn(Color color, IconData icon, String label) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Container(
          margin: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }
}

这个函数直接将图标添加到这列里。文本在以一个仅有上间距的 Container 容器中,使得文本与图标分隔开。

The function adds the icon directly to the column. The text is inside a Container with a top-only margin, separating the text from the icon.

通过调用函数并传递针对某列的颜色,Icon 图标和文本,来构建包含这些列的行。然后在行的主轴方向通过使用 MainAxisAlignment.spaceEvenly,将剩余的空间均分到每列各自的前后及中间。只需在 build() 方法中的 titleSection 声明下添加如下代码:

Build the row containing these columns by calling the function and passing the color, Icon, and text specific to that column. Align the columns along the main axis using MainAxisAlignment.spaceEvenly to arrange the free space evenly before, between, and after each column. Add the following code just below the titleSection declaration inside the build() method:

lib/main.dart (buttonSection)
Color color = Theme.of(context).primaryColor;

Widget buttonSection = Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    _buildButtonColumn(color, Icons.call, 'CALL'),
    _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
    _buildButtonColumn(color, Icons.share, 'SHARE'),
  ],
);

添加按钮部分到 body 属性中去:

Add the button section to the body:

{step2 → step3}/lib/main.dart
@@ -48,3 +59,3 @@
48
59
  return MaterialApp(
49
60
  title: 'Flutter layout demo',
50
61
  home: Scaffold(
@@ -54,8 +65,9 @@
54
65
  body: Column(
55
66
  children: [
56
67
  titleSection,
68
+ buttonSection,
57
69
  ],
58
70
  ),
59
71
  ),
60
72
  );
61
73
  }

第四步: 实现文本区域

Step 4: Implement the text section

将文本区域定义为一个变量,将文本放置到一个 Container 容器中,然后为每条边添加内边距。只需在 buttonSection 声明下添加如下代码:

Define the text section as a variable. Put the text in a Container and add padding along each edge. Add the following code just below the buttonSection declaration:

lib/main.dart (textSection)
Widget textSection = const Padding(
  padding: EdgeInsets.all(32),
  child: Text(
    'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
    'Alps. Situated 1,578 meters above sea level, it is one of the '
    'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
    'half-hour walk through pastures and pine forest, leads you to the '
    'lake, which warms to 20 degrees Celsius in the summer. Activities '
    'enjoyed here include rowing, and riding the summer toboggan run.',
    softWrap: true,
  ),
);

通过设置 softwrap 为 true,文本将在填充满列宽后在单词边界处自动换行。

By setting softwrap to true, text lines will fill the column width before wrapping at a word boundary.

添加文本部分到 body 属性:

Add the text section to the body:

{step3 → step4}/lib/main.dart
@@ -59,3 +72,3 @@
59
72
  return MaterialApp(
60
73
  title: 'Flutter layout demo',
61
74
  home: Scaffold(
@@ -66,6 +79,7 @@
66
79
  children: [
67
80
  titleSection,
68
81
  buttonSection,
82
+ textSection,
69
83
  ],
70
84
  ),
71
85
  ),

第五步: 实现图片区域

Step 5: Implement the image section

四个列元素中的三个已经完成了,只剩下图片部分了。如下添加图片文件到示例工程中:

Three of the four column elements are now complete, leaving only the image. Add the image file to the example:

现在你可以在你的代码中引用该图片了:

Now you can reference the image from your code:

{step4 → step5}/lib/main.dart
@@ -77,6 +77,12 @@
77
77
  ),
78
78
  body: Column(
79
79
  children: [
80
+ Image.asset(
81
+ 'images/lake.jpg',
82
+ width: 600,
83
+ height: 240,
84
+ fit: BoxFit.cover,
85
+ ),
80
86
  titleSection,
81
87
  buttonSection,
82
88
  textSection,

BoxFit.cover 告诉系统图片应当尽可能等比缩小到刚好能够覆盖住整个渲染 box。

BoxFit.cover tells the framework that the image should be as small as possible but cover its entire render box.

第六步: 最终的收尾

Step 6: Final touch

在最后的步骤中,需要在一个 ListView 中排列好所有的元素,而不是在一个 Column 中,因为当 app 运行在某个小设备上时,ListView 支持 app body 的滚动。

In this final step, arrange all of the elements in a ListView, rather than a Column, because a ListView supports app body scrolling when the app is run on a small device.

{step5 → step6}/lib/main.dart
@@ -72,13 +77,13 @@
72
77
  return MaterialApp(
73
78
  title: 'Flutter layout demo',
74
79
  home: Scaffold(
75
80
  appBar: AppBar(
76
81
  title: const Text('Flutter layout demo'),
77
82
  ),
78
- body: Column(
83
+ body: ListView(
79
84
  children: [
80
85
  Image.asset(
81
86
  'images/lake.jpg',
82
87
  width: 600,
83
88
  height: 240,
84
89
  fit: BoxFit.cover,

Dart code: main.dart
Image: images
Pubspec: pubspec.yaml

大功告成!当你热加载 app 时,你应当可以看到和本页开头截图一样的 app 布局了。

That’s it! When you hot reload the app, you should see the same app layout as the screenshot at the top of this page.

你可以参考文档 为你的 Flutter 应用加入交互体验 来给这个布局增加交互。

You can add interactivity to this layout by following Adding Interactivity to Your Flutter App.

创建响应式和自适应的应用

目录

Flutter 的首要目标,是构建一个可以使用单一代码来源,开发在所有平台上都有着良好的视觉和体验的应用的框架。

One of Flutter’s primary goals is to create a framework that allows you to develop apps from a single codebase that look and feel great on any platform.

这意味着你的应用可能会在不同大小的屏幕上使用,从智能手表,到可折叠的双屏设备,再到高清显示器。

This means that your app may appear on screens of many different sizes, from a watch, to a foldable phone with two screens, to a high def monitor.

通常这样的考量被分为两种概念:自适应响应式。理想条件下,你的应用应该两者兼具,但是它们究竟代表了什么?这两种概念有些类似,但并不是同一种含义。

Two terms that describe concepts for this scenario are adaptive and responsive. Ideally, you’d want your app to be both but what, exactly, does this mean? These terms are similar, but they are not the same.

自适应应用和响应式应用的区别

The difference between an adaptive and a responsive app

自适应响应式 可以看作应用里的两种维度:你的应用可能是自适应的,但不是响应式的,又或是反行其道。当然,你的应用可能既自适应又为响应式,也可能两者均未实现。

Adaptive and responsive can be viewed as separate dimensions of an app: you can have an adaptive app that is not responsive, or vice versa. And, of course, an app can be both, or neither.

响应式
通常来说,一个 响应式 应用的布局会根据可用的屏幕大小而调整。常见的场景是在用户重新调整窗口大小或旋转屏幕时,重新布局 UI。对于需要在多种设备(手表、手机、平板、笔记本或台式机)上运行的应用而言,这是必要的要素。

Responsive
Typically, a responsive app has had its layout tuned for the available screen size. Often this means (for example), re-laying out the UI if the user resizes the window, or changes the device’s orientation. This is especially necessary when the same app can run on a variety of devices, from a watch, phone, tablet, to a laptop or desktop computer.

自适应
应用以 自适应 的方式在不同的设备上(移动端和桌面端)运行,需要同时处理鼠标、键盘和触控输入。这也意味着应用的视觉密度、组件的选择(层级菜单或底部抽屉)、平台特定的行为(例如置顶的窗口)等内容将在不同的平台上有一定的差异。

Adaptive
Adapting an app to run on different device types, such as mobile and desktop, requires dealing with mouse and keyboard input, as well as touch input. It also means there are different expectations about the app’s visual density, how component selection works (cascading menus vs bottom sheets, for example), using platform-specific features (such as top-level windows), and more.

构建一个响应式的 Flutter 应用

Creating a responsive Flutter app

Flutter 让你能够构建自动适配屏幕大小和旋转方向的应用。

Flutter allows you to create apps that self-adapt to the device’s screen size and orientation.

构建响应式设计的 Flutter 应用,有以下两种较为基础的方式:

There are two basic approaches to creating Flutter apps with responsive design:

使用 LayoutBuilder
通过它的 builder 属性,你可以得到一个 BoxConstraints 对象。你可以检查约束里的属性,来决定如何进行显示。例如,如果约束里的 maxWidth 超过了你的宽度分界点,你可以返回一个 Scaffold,它包含一列内容,左侧是一个列表。如果约束更小,则返回一个列表在抽屉里的 Scaffold。你也可以根据你的设备高度、屏幕的比例或者其他的属性,来调整你的显示。当约束改变时(例如用户旋转了手机,或是在 Android N 上将应用放置到 tile UI 中)构建方法会运行。

Use the LayoutBuilder class
From its builder property, you get a BoxConstraints object. Examine the constraint’s properties to decide what to display. For example, if your maxWidth is greater than your width breakpoint, return a Scaffold object with a row that has a list on the left. If it’s narrower, return a Scaffold object with a drawer containing that list. You can also adjust your display based on the device’s height, the aspect ratio, or some other property. When the constraints change (for example, the user rotates the phone, or puts your app into a tile UI in Nougat), the build function runs.

在构建方法中使用 MediaQuery.of() 方法
这个方法可以获取到当前应用(基于上下文)的尺寸、旋转方向等信息。如果你需要基于完整的上下文信息进行布局决策,而不是基于特定的 widget,这个方法将非常有用。同样的,如果应用的尺寸发生了改变,构建方法也会自动执行。

Use the MediaQuery.of() method in your build functions
This gives you the size, orientation, etc, of your current app. This is more useful if you want to make decisions based on the complete context rather than on just the size of your particular widget. Again, if you use this, then your build function automatically runs if the user somehow changes the app’s size.

以下是其他有助于构建响应式界面的 widget:

Other useful widgets and classes for creating a responsive UI:

更多资源

Other resources

想了解更多信息,以下是一些来自社区贡献的资源:

For more information, here are a few resources, including contributions from the Flutter community:

创建自适应的 Flutter 应用

Creating an adaptive Flutter app

你可以阅读由 gskinner 团队撰写的 构建自适应的应用 了解更多关于构建自适应应用的内容。

Learn more about creating an adaptive Flutter app with Building adaptive apps, written by the gskinner team.

你也可以观看下面几期关于自适应布局的 The Boring Show:

You might also check out the following episodes of The Boring Show:

Adaptive layouts

Adaptive layouts, part 2

想要尝试精美的自适应应用,可以下载由 gskinner 和 Flutter 团队共建的剪贴板应用 Flutter Folio:

For an excellent example of an adaptive app, check out Flutter Folio, a scrapbooking app created in collaboration with gskinner and the Flutter team:

Folio 应用的源代码 也可以在 GitHub 找到,你可以阅读 gskinner 的博客 了解更多内容。

The Folio source code is also available on GitHub. Learn more on the gskinner blog.

更多资源

Other resources

你可以在以下的资源中了解更多关于如何构建自适应平台应用的内容:

You can learn more about creating platform adaptive apps in the following resources:

构建自适应的应用

目录

概览

Overview

Flutter 为在移动端、桌面端和 Web 端使用同样的代码构建应用创造了新的机会。伴随着机会而来的,是新的挑战。你可能会希望你的应用既能在尽可能复用的情况下自适应多个平台,又能保证流畅且无缝的体验,还可以让用户保持一致的使用习惯。这样的应用不仅仅是为了多个平台而构建的,它能完全地自适应平台的变化。

Flutter provides new opportunities to build apps that can run on mobile, desktop, and the web from a single codebase. However, with these opportunities, come new challenges. You want your app to feel familiar to users, adapting to each platform by maximizing usability and ensuring a comfortable and seamless experience. That is, you need to build apps that are not just multiplatform, but are fully platform adaptive.

在构建平台自适应的应用时,有众多的考量因素,总的来说分为以下几类:

There are many considerations for developing platform-adaptive apps, but they fall into three major categories:

指南将通过代码片段,详细说明三个类别的概念。若你想了解这些概念的实际落地情况,可以参考 FlokkFolio 示例。

This page covers all three categories in detail using code snippets to illustrate the concepts. If you’d like to see how these concepts come together, check out the Flokk and Folio examples that were built using the concepts described here.

Original demo code for adaptive app development techniques from flutter-adaptive-demo.

构建自适应的布局

Building adaptive layouts

在构建多平台的应用时,首要考虑的是如何针对不同大小的设备进行尺寸适配。

One of the first things you must consider when bringing your app to multiple platforms is how to adapt it to the various sizes and shapes of the screens that it will run on.

布局 widgets

Layout widgets

如果你已经开发过应用或网站,那你可能已经熟悉如何构建自适应的界面。好消息是,对于 Flutter 开发者而言,有非常多的 widgets 让构建更为简单。

If you’ve been building apps or websites, you’re probably familiar with creating responsive interfaces. Luckily for Flutter developers, there are a large set of widgets to make this easier.

Flutter 中最有用的部分布局 widgets 包括:

Some of Flutter’s most useful layout widgets include:

单子级 (Single child)

Single child

多子级 (Multi child)

Multichild

查看 布局 widgets 了解更多的 widgets 和代码示例。

To see more available widgets and example code, see Layout widgets.

视觉密度

Visual density

不同的设备会提供不同级别的显示密度,使得操作的命中区域也要随之变化。 Flutter 的 VisualDensity 类可以让你快速地调整整个应用的视图密度,比如在可触控设备上放大一个按钮(使其更容易被点击)。

不同的设备会提供不同级别的显示密度,使得操作的命中区域也要随之变化。 Flutter 的 VisualDensity 类可以让你快速地调整整个应用的视图密度,比如在可触控设备上放大一个按钮(使其更容易被点击)。

Different input devices offer various levels of precision, which necessitate differently sized hit areas. Flutter’s VisualDensity class makes it easy to adjust the density of your views across the entire application, for example, by making a button larger (and therefore easier to tap) on a touch device.

在你改变 MaterialAppVisualDensity 时,已支持 VisualDensityMaterialComponents 会以动画过渡的形式改变其自身的密度。水平和垂直方向的密度默认都为 0.0,你可以将它设置为任意的正负值,这样就可以通过调整密度轻松地调整你的 UI:

When you change the VisualDensity for your MaterialApp, MaterialComponents that support it animate their densities to match. By default, both horizontal and vertical densities are set to 0.0, but you can set the densities to any negative or positive value that you want. By switching between different densities, you can easily adjust your UI:

Adaptive scaffold

若想使用自定义的视觉密度,请在你的 MaterialApp 的主题中进行设置:

To set a custom visual density, inject the density into your MaterialApp theme:

double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density =
    VisualDensity(horizontal: densityAmt, vertical: densityAmt);
return MaterialApp(
  theme: ThemeData(visualDensity: density),
  home: MainAppScaffold(),
  debugShowCheckedModeBanner: false,
);

若想在你的视图中使用 VisualDensity,你可以向上查找:

To use VisualDensity inside your own views, you can look it up:

VisualDensity density = Theme.of(context).visualDensity;

在密度变化时,容器不仅能自动地对其做出反应,还会结合动画进行过渡变化。所有的组件都会联系在一起,使整个应用平滑过渡。

Not only does the container react automatically to changes in density, it also animates when it changes. This ties together your custom components, along with the built-in components, for a smooth transition effect across the app.

我们可以看到,VisualDensity 是没有单位的,所以在不同的视图上可能有不同的含义。在以上的例子中,1 个单位的密度等同于 6 个逻辑像素。具体的处理完全由你的视图自行决定。无单位的设计让它可以处理通用情况,能在大部分的场景下使用。

As shown, VisualDensity is unit-less, so it can mean different things to different views. In this example, 1 density unit equals 6 pixels, but this is totally up to your views to decide. The fact that it is unit-less makes it quite versatile, and it should work in most contexts.

值得注意的是,在 Material 的组件中,1 个单位的视觉密度通常等于 4 个逻辑像素。你可以查看 VisualDensity API 文档了解更多支持视觉密度的组件。若想了解视觉密度的通用原则,请查看 Material Design 指南

It’s worth noting that the Material Components generally use a value of around 4 logical pixels for each visual density unit. For more information about the supported components, see VisualDensity API. For more information about density principles in general, see the Material Design guide.

基于上下文的布局

Contextual layout

如果你需要的不仅是密度的变化,并且没有找到一个满足需求的 widget,那么你可以使用代码进行更细化的控制、计算尺寸、切换 widgets 或是完全重新构建你的 UI 适配对应的外形结构。

If you need more than density changes and can’t find a widget that does what you need, you can take a more procedural approach to adjust parameters, calculate sizes, swap widgets, or completely restructure your UI to suit a particular form factor.

基于屏幕大小的分界点

Screen-based breakpoints

最简单的代码控制布局方式是基于屏幕尺寸来定义分界点。在 Flutter 中,你可以使用 MediaQuery API 实现这些分界点。具体需要使用的大小并没有作出硬性规定,下方是一些通用的值:

The simplest form of procedural layouts uses screen-based breakpoints. In Flutter, this can be done with the MediaQuery API. There are no hard and fast rules for the sizes to use here, but these are general values:

class FormFactor {
  static double desktop = 900;
  static double tablet = 600;
  static double handset = 300;
}

使用分界点可以让你通过简单的判断快速确定设备的类型:

Using breakpoints, you can set up a simple system to determine the device type:

ScreenType getFormFactor(BuildContext context) {
  // Use .shortestSide to detect device type regardless of orientation
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > FormFactor.desktop) return ScreenType.Desktop;
  if (deviceWidth > FormFactor.tablet) return ScreenType.Tablet;
  if (deviceWidth > FormFactor.handset) return ScreenType.Handset;
  return ScreenType.Watch;
}

又或者,你可以对大小类型进行更深层次的抽象,并且按照从小到大的方式定义:

As an alternative, you could abstract it more and define it in terms of small to large:

enum ScreenSize { Small, Normal, Large, ExtraLarge }

ScreenSize getSize(BuildContext context) {
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > 900) return ScreenSize.ExtraLarge;
  if (deviceWidth > 600) return ScreenSize.Large;
  if (deviceWidth > 300) return ScreenSize.Normal;
  return ScreenSize.Small;
}

使用基于屏幕大小的分界点的最佳场景,是在应用的顶层进行尺寸决策。在需要改变视觉密度、边距或者字体大小时,定义全局的基数是最好的方式。

Screen-based breakpoints are best used for making top-level decisions in your app. Changing things like visual density, paddings, or font-sizes are best when defined on a global basis.

你也可以利用分界点重新组织顶层的 widget 结构。例如,你可以判断用户是否使用手持设备,来切换垂直或水平的布局:

You can also use screen-based breakpoints to reflow your top-level widget trees. For example, you could switch from a vertical to a horizontal layout when the user isn’t on a handset:

bool isHandset = MediaQuery.of(context).size.width < 600;
return Flex(
    children: [Text("Foo"), Text("Bar"), Text("Baz")],
    direction: isHandset ? Axis.vertical : Axis.horizontal);

在其他的 widget 中,你也可以切换部分子级 widget:

In another widget, you might swap some of the children completely:

Widget foo = Row(
  children: [
    ...isHandset ? _getHandsetChildren() : _getNormalChildren(),
  ],
);

使用 LayoutBuilder 提升布局灵活性

Use LayoutBuilder for extra flexibility

尽管对于全屏页面或者全局的布局决策而言,判断整个屏幕大小非常有效,但对于内嵌的子视图而言,并不一定是合理的方案。子视图通常有自己的分界点,并且只关心它们可用的渲染空间。

Even though checking total screen size is great for full-screen pages or making global layout decisions, it’s often not ideal for nested subviews. Often, subviews have their own internal breakpoints and care only about the space that they have available to render.

在 Flutter 内处理这类场景最简单的做法是使用 LayoutBuilderLayoutBuilder 让 widget 可以根据其父级的限制进行调整,相比依赖全局的尺寸限制而言更为通用。

The simplest way to handle this in Flutter is using the LayoutBuilder class. LayoutBuilder allows a widget to respond to incoming local size constraints, which can make the widget more versatile than if it depended on a global value.

之前的示例可以使用 LayoutBuilder 重写:

The previous example could be rewritten using LayoutBuilder:

Widget foo = LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
  bool useVerticalLayout = constraints.maxWidth < 400.0;
  return Flex(
    children: [
      Text("Hello"),
      Text("World"),
    ],
    direction: useVerticalLayout ? Axis.vertical : Axis.horizontal,
  );
});

现在这个 widget 可以组装在侧边面板、弹框又或是全屏视图中,并且根据尺寸自适应布局。

This widget can now be composed within a side panel, dialog, or even a full-screen view, and adapt its layout to whatever space is provided.

设备细分

Device segmentation

有时你可能需要根据实际运行的平台进行布局处理,而不是基于大小。例如,在构建自定义的标题栏时,你可能需要判断设备的平台来处理布局,以防被原生窗口的按钮遮挡。

There are times when you want to make layout decisions based on the actual platform you’re running on, regardless of size. For example, when building a custom title bar, you might need to check the operating system type and tweak the layout of your title bar, so it doesn’t get covered by the native window buttons.

想判断应用当前所处的平台,你可以使用 Platform API 和 kIsWeb 组合进行判断:

To determine which combination of platforms you’re on, you can use the Platform API along with the kIsWeb value:

bool get isMobileDevice => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
bool get isDesktopDevice =>
    !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
bool get isMobileDeviceOrWeb => kIsWeb || isMobileDevice;
bool get isDesktopDeviceOrWeb => kIsWeb || isDesktopDevice;

在构建 Web 平台应用时,由于 dart.io package 不支持 Web 平台,导致使用 Platform API 时会异常。所以在上面的代码中,会首先判断是否在 Web 平台,基于这个条件,在 Web 平台上永远不会调用 Platform API。

The Platform API can’t be accessed from web builds without throwing an exception, because the dart.io package is not supported on the web target. As a result, this code checks for web first, and because of short-circuiting, Dart will never call Platform on web targets.

使用单一来源控制样式

Single source of truth for styling

使用单一的来源对样式进行维护,可以让你更简便地控制边距、间距、圆角、字体等样式值。你可以利用一些帮助类进行实现:

You’ll probably find it easier to maintain your views if you create a single source of truth for styling values like padding, spacing, corner shape, font sizes, and so on. This can be done easily with some helper classes:

class Insets {
  static const double xsmall = 3;
  static const double small = 4;
  static const double medium = 5;
  static const double large = 10;
  static const double extraLarge = 20;
  // etc
}

class Fonts {
  static const String raleway = 'Raleway';
  // etc
}

class TextStyles {
  static const TextStyle raleway = const TextStyle(
    fontFamily: Fonts.raleway,
  );
  static TextStyle buttonText1 =
      TextStyle(fontWeight: FontWeight.bold, fontSize: 14);
  static TextStyle buttonText2 =
      TextStyle(fontWeight: FontWeight.normal, fontSize: 11);
  static TextStyle h1 = TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
  static TextStyle h2 = TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
  static late TextStyle body1 = raleway.copyWith(color: Color(0xFF42A5F5));
  // etc
}

这些常量可以用来替代硬编码的值:

These constants can then be used in place of hard-coded numeric values:

return Padding(
  padding: EdgeInsets.all(Insets.small),
  child: Text('Hello!', style: TextStyles.body1),
);

由于所有的视图都引用了相同设计系统的规范,它们通常看起来更一致且更顺畅。与其进行容易出错的搜索替换,你可以将平台对应样式值的修改集中在一处。使用共享的规则也对设计的一致性有所帮助。

With all views referencing the same shared-design system rules, they tend to look better and more consistent. Making a change or adjusting a value for a specific platform can be done in a single place, instead of using an error-prone search and replace. Using shared rules has the added benefit of helping enforce consistency on the design side.

常见的设计类型里,如下这些类别可以以这样的方式进行组织:

Some common design system categories that can be represented this way are:

当然,上述的例子也有一些例外:在应用中只使用了一次的值。将这些值放在样式规则里属实无用之举,但可以考虑它们是否能从现有的值延伸(例如 padding + 1.0)。你也可以留意一些有着相同意义复用的值,这些值也许可以添加到全局的样式规则里。

Like most rules, there are exceptions: one-off values that are used nowhere else in the app. There is little point in cluttering up the styling rules with these values, but it’s worth considering if they should be derived from an existing value (for example, padding + 1.0). You should also watch for reuse or duplication of the same semantic values. Those values should likely be added to the global styling ruleset.

针对不同外形屏幕的特性进行设计

Design to the strengths of each form factor

除了屏幕尺寸以外,你也应当花时间,针对各种不同外形屏幕的优劣点进行设计。支持多平台的应用,并不能在所有的设备上都提供理想的体验。实际开发时,可以考虑某些特定的功能是否合理,也可以考虑在某些平台上移除特定的功能。

Beyond screen size, you should also spend time considering the unique strengths and weaknesses of different form factors. It isn’t always ideal for your multiplatform app to offer identical functionality everywhere. Consider whether it makes sense to focus on specific capabilities, or even remove certain features, on some device categories.

举个例子,移动设备是十分便携的,一般还配有摄像头,但它们并不适合深度的内容创作工作。基于这个前提,你的应用可以更侧重于内容捕获,并使用位置信息对其进行标记,配上移动端的界面,而另一方面,在平板和桌面界面上专注于组织和操作产出的内容。

For example, mobile devices are portable and have cameras, but they aren’t well suited for detailed creative work. With this in mind, you might focus more on capturing content and tagging it with location data for a mobile UI, but focus on organizing or manipulating that content for a tablet or desktop UI.

另一个例子是充分利用 Web 平台的快速分享能力。如果你正在部署 Web 应用,可以考虑哪些页面会使用 deep link,并根据配置来设计应用的导航。

Another example is leveraging the web’s extremely low barrier for sharing. If you’re deploying a web app, decide which deep links to support, and design your navigation routes with those in mind.

此处的关键点在于,如何发挥每个平台的长处,寻找平台可以利用的特有功能。

The key takeaway here is to think about what each platform does best and see if there are unique capabilities you can leverage.

通过构建桌面应用程序进行快速测试

Use desktop build targets for rapid testing

测试自适应界面的最快方式,是利用桌面端快速进行构建。

One of the most effective ways to test adaptive interfaces is to take advantage of the desktop build targets.

在桌面上运行应用时,你可以在应用运行时轻易地改变窗口的大小,预览多种尺寸的布局。配上热重载,能极大程度地加快响应式开发的速度。

When running on a desktop, you can easily resize the window while the app is running to preview various screen sizes. This, combined with hot reload, can greatly accelerate the development of a responsive UI.

Adaptive scaffold 2

优先处理触摸操作

Solve touch first

在移动端构建优良的触摸交互式 UI 通常比传统的桌面端更为困难,因为它缺少类似右键单击、滚轮或键盘快捷键这样的快速输入设备。

Building a great touch UI can often be more difficult than a traditional desktop UI due, in part, to the lack of input accelerators like right-click, scroll wheel, or keyboard shortcuts.

在一开始就专注于提升触摸体验的 UI,足以应对这样的挑战。你依旧可以使用桌面端来提高你的开发效率,但要记得时不时切换回移动端,验证开发的内容是否正常。

One way to approach this challenge is to focus initially on a great touch-oriented UI. You can still do most of your testing using the desktop target for its iteration speed. But, remember to switch frequently to a mobile device to verify that everything feels right.

完善了触摸界面后,你可以调整面向鼠标用户的视觉密度,然后对所有的输入设备进行分层。这些输入设备应当作为加快你的应用使用速度的途径。在这里需要考虑的应当是用户对于应用体验的期望,并在应用中合理地实现这些期望。

After you have the touch interface polished, you can tweak the visual density for mouse users, and then layer on all the additional inputs. Approach these other inputs as accelerator—alternatives that make a task faster. The important thing to consider is what a user expects when using a particular input device, and work to reflect that in your app.

输入

Input

当然,应用只适配了界面是远远不够的,你还需要适配各种用户的输入操作。鼠标和键盘提供了触摸设备不具备的输入方式,例如滚轮、右键点击、悬停交互、Tab 遍历切换和键盘快捷键。

Of course, it isn’t enough to just adapt how your app looks, you also have to support varying user inputs. The mouse and keyboard introduce input types beyond those found on a touch device—like scroll wheel, right-click, hover interactions, tab traversal, and keyboard shortcuts.

滚轮

Scroll wheel

ScrollViewListView 这样的滚动 widget 默认支持滚轮行为,而大部分可滚动的自定义 widget 都是基于它们构建的,所以也同样支持。

Scrolling widgets like ScrollView or ListView support the scroll wheel by default, and because almost every scrollable custom widget is built using one of these, it works with them as well.

如果你需要实现自定义的滑动行为,可以使用 Listener widget,通过它你可以完全自定义 UI 如何响应滚轮行为。

If you need to implement custom scroll behavior, you can use the Listener widget, which lets you customize how your UI reacts to the scroll wheel.

return Listener(
    onPointerSignal: (event) {
      if (event is PointerScrollEvent) print(event.scrollDelta.dy);
    },
    child: ListView());

Tab 遍历切换和焦点交互

Tab traversal and focus interactions

使用键盘的用户,可能会希望通过 Tab 键在应用中快速导航,特别是对有动效和视觉障碍的用户,他们几乎完全依赖于键盘导航。

Users with physical keyboards expect that they can use the tab key to quickly navigate your application, and users with motor or vision differences often rely completely on keyboard navigation.

在考虑 Tab 遍历切换时,有两点需要注意:焦点如何在 widget 之间遍历,以及 widget 聚焦时的突出显示。

There are two considerations for tab interactions: how focus moves from widget to widget, known as traversal, and the visual highlight shown when a widget is focused.

大部分内置的组件,类似于按钮和输入框,都默认支持遍历和高亮。如果你想让自己的 widget 包含在遍历中,你可以利用 FocusableActionDetector 进行控制。它将 ActionsShortcutsMouseRegionFocus 的能力进行了整合,创建出一个可以定义行为和键位绑定,并且提供聚焦和悬浮高亮事件回调的 widget。

Most built-in components, like buttons and text fields, support traversal and highlights by default. If you have your own widget that you want included in traversal, you can use the FocusableActionDetector widget to create your own controls. It combines the functionality of Actions, Shortcuts, MouseRegion, and Focus widgets to create a detector that defines actions and key bindings, and provides callbacks for handling focus and hover highlights.

class _BasicActionDetectorState extends State<BasicActionDetector> {
  bool _hasFocus = false;
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      onFocusChange: (value) => setState(() => _hasFocus = value),
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(onInvoke: (Intent intent) {
          print("Enter or Space was pressed!");
        }),
      },
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          FlutterLogo(size: 100),
          // Position focus in the negative margin for a cool effect
          if (_hasFocus)
            Positioned(
                left: -4,
                top: -4,
                bottom: -4,
                right: -4,
                child: _roundedBorder())
        ],
      ),
    );
  }
}

控制遍历的顺序

Controlling traversal order

想要控制用户按下 Tab 键时的 widget 切换顺序,你可以使用 FocusTraversalGroup 来指定树中的区域,作为切换时的组别。

To get more control over the order that widgets are focused on when the user presses tab, you can use FocusTraversalGroup to define sections of the tree that should be treated as a group when tabbing.

例如,你可能想要用户逐个切换所有的输入框,最后再切换到提交按钮:

For example, you might to tab through all the fields in a form before tabbing to the submit button:

return Column(children: [
  FocusTraversalGroup(
    child: MyFormWithMultipleColumnsAndRows(),
  ),
  SubmitButton(),
]);

Flutter 有几种内置的方法对 widget 和组别进行遍历,默认使用的是 ReadingOrderTraversalPolicy 类。这个类通常可以正常使用,你也可以创建另一个 TraversalPolicy 或创建一个自定义的规则,对它进行定义。

Flutter has several built-in ways to traverse widgets and groups, defaulting to the ReadingOrderTraversalPolicy class. This class usually works well, but it’s possible to modify this using another predefined TraversalPolicy class or by creating a custom policy.

提升用户操作速度的键盘

Keyboard accelerators

除了使用 Tab 遍历元素以外,桌面和 Web 用户还习惯将为各种操作绑定键盘快捷键。无论是 Delete 键进行快速删除,还是 Control+N 新建文档,你都需要认真考虑用户对这些操作的期望。键盘是非常强力的输入工具,所以请尽可能让它发挥最大的作用和效果。用户会给予高度评价。

In addition to tab traversal, desktop and web users are accustomed to having various keyboard shortcuts bound to specific actions. Whether it’s the Delete key for quick deletions or Control+N for a new document, be sure to consider the different accelerators your users expect. The keyboard is a powerful input tool, so try to squeeze as much efficiency from it as you can. Your users will appreciate it!

根据目标的不同,在 Flutter 中可以通过几种方式实现利用键盘提升用户操作速度。

Keyboard accelerators can be accomplished in a few ways in Flutter depending on your goals.

如果你已经有一个包含焦点的 widget,例如 TextField 或者 Button,你可以嵌套一个 RawKeyboardListener 监听键盘事件:

If you have a single widget like a TextField or a Button that already has a focus node, you can wrap it in a RawKeyboardListener and listen for keyboard events:

  @override
  Widget build(BuildContext context) {
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        if (event is RawKeyDownEvent) {
          print(event.logicalKey);
        }
        return KeyEventResult.ignored;
      },
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: 400),
        child: TextField(
          decoration: InputDecoration(
            border: OutlineInputBorder(),
          ),
        ),
      ),
    );
  }
}

如果你想将一组键盘快捷键应用到更大范围的 widget,你可以使用 Shortcuts widget:

If you’d like to apply a set of keyboard shortcuts to a large section of the tree, you can use the Shortcuts widget:

// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
  const CreateNewItemIntent();
}

Widget build(BuildContext context) {
  return Shortcuts(
    // Bind intents to key combinations
    shortcuts: <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    },
    child: Actions(
      // Bind intents to an actual method in your code
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
            onInvoke: (CreateNewItemIntent intent) => _createNewItem()),
      },
      // Your sub-tree must be wrapped in a focusNode, so it can take focus.
      child: Focus(
        autofocus: true,
        child: Container(),
      ),
    ),
  );
}

Shortcuts widget 非常有用,因为它会让 widget 树的这一分支或它的子级仅在有焦点且可见时触发快捷方式。

The Shortcuts widget is useful because it only allows shortcuts to be fired when this widget tree or one of its children has focus and is visible.

最后,你还可以全局添加监听。这样的监听可以用于始终需要监听,且为应用全局的快捷键,或是在任何时候(无论是否已聚焦)都接收快捷键的部分。使用 RawKeyboard 添加全局监听非常简单:

The final option is a global listener. This listener can be used for always-on, app-wide shortcuts or for panels that can accept shortcuts whenever they’re visible (regardless of their focus state). Adding global listeners is easy with RawKeyboard:

void initState() {
  super.initState();
  RawKeyboard.instance.addListener(_handleKey);
}

@override
void dispose() {
  RawKeyboard.instance.removeListener(_handleKey);
  super.dispose();
}

要想在全局监听中判断组合按键,你可以使用 RawKeyboard.instance.keysPressed 这个 Map 进行判断。例如下面这个方法,可以判断是否已经按下了指定的按键:

To check key combinations with the global listener, you can use the RawKeyboard.instance.keysPressed map. For example, a method like the following can check whether any of the provided keys are being held down:

static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys.intersection(RawKeyboard.instance.keysPressed).isNotEmpty;
}

将它们合并判断,你就可以在 Shift+N 同时按下时触发行为:

Putting these two things together, you can fire an action when Shift+N is pressed:

void _handleKey(event) {
  if (event is RawKeyDownEvent) {
    bool isShiftDown = isKeyDown({
      LogicalKeyboardKey.shiftLeft,
      LogicalKeyboardKey.shiftRight,
    });
    if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
      _createNewItem();
    }
  }
}

使用静态的监听时有一件值得注意的事情,当用户在输入框中输入内容,或关联的 widget 从视图中隐藏时,通常需要禁用监听。与 ShortcutsRawKeyboardListener 不同,你需要自己对它们进行管理。当你在为 Delete 键构建一个删除或退格行为的监听时,需要尤其注意,因为用户可能会在 TextField 中输入内容时受到影响。

One note of caution when using the static listener, is that you often need to disable it when the user is typing in a field or when the widget it’s associated with is hidden from view. Unlike with Shortcuts or RawKeyboardListener, this is your responsibility to manage. This can be especially important when you’re binding a Delete/Backspace accelerator for Delete, but then have child TextFields that the user might be typing in.

鼠标进入、移出和悬停事件

Mouse enter, exit, and hover

在桌面平台上,常会在鼠标悬停在内容上时,改变光标以表明不同的功能用途。例如,你会在鼠标悬停的按钮上看到手指光标,或是在悬停的文字上看到一个 I

On desktop, it’s common to change the mouse cursor to indicate the functionality about the content the mouse is hovering over. For example, you usually see a hand cursor when you hover over a button, or an I cursor when you hover over text.

Material 系列组件内置了对标准的按钮和文字的光标支持。你可以使用 MouseRegion 在你自己的 widget 上改变光标。

The Material Component set has built-in support for your standard button and text cursors. To change the cursor from within your own widgets, use MouseRegion:

// Show hand cursor
return MouseRegion(
  cursor: SystemMouseCursors.click,
  // Request focus when clicked
  child: GestureDetector(
    onTap: () {
      Focus.of(context).requestFocus();
      _submit();
    },
    child: Logo(showBorder: hasFocus),
  ),
);

MouseRegion 对于创建自定义翻转和悬停效果也很有用:

MouseRegion is also useful for creating custom rollover and hover effects:

return MouseRegion(
  onEnter: (_) => setState(() => _isMouseOver = true),
  onExit: (_) => setState(() => _isMouseOver = false),
  onHover: (PointerHoverEvent e) => print(e.localPosition),
  child: Container(
    height: 500,
    color: _isMouseOver ? Colors.blue : Colors.black,
  ),
);

平台行为习惯与规范

Idioms and norms

最后,我们需要为自适应应用考虑平台标准。每个平台都有其不同的行为习惯与规范,这些名义和事实上的标准将操作应用的方法告知了用户。在当下网络如此便利的时代,用户更倾向于更加个性化的体验,但是提供这些平台标准,依然可以带来一些显著的好处:

The final area to consider for adaptive apps is platform standards. Each platform has its own idioms and norms; these nominal or de facto standards inform user expectations of how an application should behave. Thanks, in part to the web, users are accustomed to more customized experiences, but reflecting these platform standards can still provide significant benefits:

考虑每个平台的预期交互行为

Consider expected behavior on each platform

考虑的第一步,是花一些时间思考应用在这个平台上期望的外观、表现或者行为。试着将当前能否实现的限制抛诸脑后,仅针对理想的用户体验进行逆向思考。

The first step is to spend some time considering what the expected appearance, presentation, or behavior is on this platform. Try to forget any limitations of your current implementation, and just envision the ideal user experience. Work backwards from there.

另一种思考方式,是向自己提问:「该平台的用户要想完成这个操作,需要什么样的交互?」接着开始设想如何在应用内正常且无妥协地实现它。

Another way to think about this is to ask, “How would a user of this platform expect to achieve this goal?” Then, try to envision how that would work in your app without any compromises.

如果你本身不是这个平台的常用用户,这项工作就有一定的难度。某些特定的行为和习惯,很容易会被你完全忽略。例如,一位一直使用 Android 的用户很有可能不清楚 iOS 平台的约定,同样还有 macOS、Linux 和 Windows。对于身为开发者的你来说,这些差异可能微乎其微,但对于有经验的用户来说是显而易见的。

This can be difficult if you aren’t a regular user of the platform. You might be unaware of the specific idioms and can easily miss them completely. For example, a lifetime Android user will likely be unaware of platform conventions on iOS, and the same holds true for macOS, Linux, and Windows. These differences might be subtle to you, but be painfully obvious to an experienced user.

寻找一位平台的实际用户(倡导者)

Find a platform advocate

最好为每一种适配平台指定一位负责人。理想情况下,负责人以他们熟悉的平台为主,提供他们对平台特有的看法和意见。若想减少人员,兼顾角色,可以安排一位支持 Windows 和 Android,一位支持 Linux 和 Web,一位支持 Mac 和 iOS。

If possible, assign someone as an advocate for each platform. Ideally, your advocate uses the platform as their primary device, and can offer the perspective of a highly opinionated user. To reduce the number of people, combine roles. Have one advocate for Windows and Android, one for Linux and the web, and one for Mac and iOS.

这样做的目的是为了得到持续且有效的反馈,让应用在每个平台上都能表现良好。负责人应该以挑剔的角度对平台实现进行把关。一个非常简单的例子是在对话框里,对话框本身按钮的默认位置在 Mac 和 Linux 上通常位于左侧,而在 Windows 上位于右侧。如果你不是平台的常用用户,通常会错过这样的细节。

The goal is to have constant, informed feedback so the app feels great on each platform. Advocates should be encouraged to be quite picky, calling out anything they feel differs from typical applications on their device. A simple example is how the default button in a dialog is typically on the left on Mac and Linux, but is on the right on Windows. Details like that are easy to miss if you aren’t using a platform on a regular basis.

保持应用的独特

Stay unique

应用并不一定需要默认的组件或样式来保证其符合期望的行为。许多非常流行的多平台应用都有自成一派的 UI,包括自定义按钮、选项菜单和标题栏等。

Conforming to expected behaviors doesn’t mean that your app needs to use default components or styling. Many of the most popular multiplatform apps have very distinct and opinionated UIs including custom buttons, context menus, and title bars.

跨平台样式内容越多,开发和测试就越轻松。在构建你的用户体验时,要注意平衡对它们的选择,同时还要尊重各个平台的规范。

The more you can consolidate styling and behavior across platforms, the easier development and testing will be. The trick is to balance creating a unique experience with a strong identity, while respecting the norms of each platform.

需要考虑的常见平台行为习惯与规范

Common idioms and norms to consider

让我们来快速浏览一下你可能需要考虑的规范和习惯,了解一下在 Flutter 中如何实现它们。

Take a quick look at a few specific norms and idioms you might want to consider, and how you could approach them in Flutter.

滚动条的外观和行为

Scrollbar appearance and behavior

无论是桌面端还是移动端的用户,都需要滚动条,但他们对不同平台所期待的行为是不一样的。移动端的用户希望滚动条小一些,只在滚动时出现,而桌面端的用户一般想要更大且一直显示的滚动条,同时可以点击和拖动。

Desktop and mobile users expect scrollbars, but they expect them to behave differently on different platforms. Mobile users expect smaller scrollbars that only appear while scrolling, whereas desktop users generally expect omnipresent, larger scrollbars that they can click or drag.

Flutter 内置了 Scrollbar widget,会根据当前所在的平台自适应颜色和大小。你可能会需要调整 alwaysShown 以在桌面平台上一直显示滚动条:

Flutter comes with a built-in Scrollbar widget that already has support for adaptive colors and sizes according to the current platform. The one tweak you might want to make is to toggle alwaysShown when on a desktop platform:

return Scrollbar(
  isAlwaysShown: DeviceType.isDesktop,
  controller: _scrollController,
  child: GridView.count(
      controller: _scrollController,
      padding: EdgeInsets.all(Insets.extraLarge),
      childAspectRatio: 1,
      crossAxisCount: colCount,
      children: listChildren),
);

对这些细节的把握,可以让你的应用在对应平台上体验更为良好。

This subtle attention to detail can make your app feel more comfortable on a given platform.

多选

Multi-select

跨平台的另一个存在差异的地方,是如何处理列表中的多选:

Dealing with multi-select within a list is another area with subtle differences across platforms:

static bool get isSpanSelectModifierDown =>
    isKeyDown({LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight});

要想监测不同平台的 Control 或 Command 键,你可以编写以下的代码:

To perform a platform-aware check for control or command, you can write something like this:

static bool get isMultiSelectModifierDown {
  bool isDown = false;
  if (Platform.isMacOS) {
    isDown = isKeyDown(
        {LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight});
  } else {
    isDown = isKeyDown(
        {LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight});
  }
  return isDown;
}

最后一项针对键盘用户需要考虑的是 全选 操作。如果你的列表里有很多的可选择内容,可能你的许多用户也会希望能使用 Control+A 选中所有内容。

A final consideration for keyboard users is the Select All action. If you have a large list of items of selectable items, many of your keyboard users will expect that they can use Control+A to select all the items.

触屏设备
Touch devices

在触屏设备上,多选操作通常会被简化,与在桌面上按下了 isMultiSelectModifier(多选按钮)的行为类似。

On touch devices, multi-selection is typically simplified, with the expected behavior being similar to having the isMultiSelectModifier down on the desktop. You can select or deselect items using a single tap, and will usually have a button to Select All or Clear the current selection.

在不同设备上处理多选操作,取决于你的用例是否有区分,但更重要的是为各个平台提供最好的交互模式。

How you handle multi-selection on different devices depends on your specific use cases, but the important thing is to make sure that you’re offering each platform the best interaction model possible.

可选中的文字

Selectable text

对于 Web 平台(以及小部分的桌面平台)而言,大部分能看到的文字都是可以使用鼠标选择的。如果不能选择,用户可能会感到不正常。

A common expectation on the web (and to a lesser extent desktop) is that most visible text can be selected with the mouse cursor. When text is not selectable, users on the web tend to have an adverse reaction.

幸运的是,使用 SelectableText 就可以很简单地支持选择:

Luckily, this is easy to support with the SelectableText widget:

return SelectableText('Select me!');

可以用 TextSpan 支持富文本:

To support rich text, then use TextSpan:

return SelectableText.rich(
  TextSpan(
    children: [
      TextSpan(text: 'Hello'),
      TextSpan(text: 'Bold', style: TextStyle(fontWeight: FontWeight.bold)),
    ],
  ),
);

标题栏

Title bars

在现代的桌面应用程序中,经常会有定制应用窗口的标题栏、添加 Logo 或者其他控制的需求,能节省界面对于垂直空间的占用。

On modern desktop applications, it’s common to customize the title bar of your app window, adding a logo for stronger branding or contextual controls to help save vertical space in your main UI.

Samples of title bars

Flutter 并没有内置这样的支持,但是你可以使用 bits_dojo package 禁用标题栏,并且替换成自己的。

This isn’t supported directly in Flutter, but you can use the bits_dojo package to disable the native title bars, and replace them with your own.

你可以利用这个 package 将任意 widget 应用在标题栏上,因为它是基于 Flutter 的 widget 进行设置的。如此一来,当你在应用内各个地方浏览时,标题栏都能以非常便捷的方式进行适配。

This package lets you add whatever widgets you want to the TitleBar because it uses pure Flutter widgets under the hood. This makes it easy to adapt the title bar as you navigate to different sections of the app.

上下文菜单和提示

Context menus and tooltips

在桌面平台上,通常有几种在叠加层中显示的交互组件,它们各自有不同的触发、关闭和定位方式:

On desktop, there are several interactions that manifest as a widget shown in an overlay, but with differences in how they’re triggered, dismissed, and positioned:

若你想在 Flutter 中显示一个简单的提示,你可以使用 Tooltip widget:

To show basic tooltips in Flutter, use the built-in Tooltip widget:

return const Tooltip(
  message: 'I am a Tooltip',
  child: Text('Hover over the text to show a tooltip.'),
);

Flutter 同时也为编辑和选择文字提供了内置的上下文菜单。

Flutter also provides built-in context menus when editing or selecting text.

若你想显示更高级的提示、悬浮面板或自定义的上下文菜单,你可以使用已有的 package,或利用 StackOverlay 进行构建。

To show more advanced tooltips, popup panels, or create custom context menus, you either use one of the available packages, or build it yourself using a Stack or Overlay.

可以使用的 package 包括:

Some available packages include:

尽管这些控制对于触控用户来说只是一种增强,但对于桌面用户而言,它们是必不可少的。桌面用户会期望能够右键点击其中一些内容,当场进行编辑,悬浮时查看更多信息。若你的应用并不包含这类交互,相关的用户群体可能会感到有些失望,或是认为某些地方不合理。

While these controls can be valuable for touch users as accelerators, they are essential for mouse users. These users expect to right-click things, edit content in place, and hover for more information. Failing to meet those expectations can lead to disappointed users, or at least, a feeling that something isn’t quite right.

按钮的水平排列

Horizontal button order

在 Windows 上展示一行按钮时,确认按钮会在一行的起始位置(左侧)。而在其他平台上,则是完全相反的,确认按钮显示在末尾位置(右侧)。

On Windows, when presenting a row of buttons, the confirmation button is placed at the start of the row (left side). On all other platforms, it’s the opposite. The confirmation button is placed at the end of the row (right side).

在 Flutter 里你可以很轻松地修改 RowTextDirection 来达到这个效果:

This can be easily handled in Flutter using the TextDirection property on Row:

TextDirection btnDirection =
    DeviceType.isWindows ? TextDirection.rtl : TextDirection.ltr;
return Row(
  children: [
    Spacer(),
    Row(
      textDirection: btnDirection,
      children: [
        DialogButton(
            label: "Cancel",
            onPressed: () => Navigator.pop(context, false)),
        DialogButton(
            label: "Ok", onPressed: () => Navigator.pop(context, true)),
      ],
    ),
  ],
);

Sample of embedded image

Sample of embedded image

Menu bar

桌面平台有另一种常见的内容:菜单栏。在 Windows 和 Linux 上,Chrome 的菜单栏整合在标题栏内,而在 macOS 上,菜单栏在主屏幕的顶部。

Another common pattern on desktop apps is the menu bar. On Windows and Linux, this menu lives as part of the Chrome title bar, whereas on macOS, it’s located along the top of the primary screen.

目前你可以使用一个原型插件来指定菜单栏的入口,我们希望这个功能最终能合并到 SDK 中。

Currently, you can specify custom menu bar entries using a prototype plugin, but it’s expected that this functionality will eventually be integrated into the main SDK.

值得一提的是,在 Windows 和 Linux 上,你无法将自定义的标题栏与菜单栏整合在一起。在构建自定义的标题栏时,实际上是替换了整个原生的标题栏,意味着你也同时失去了原生的菜单栏。

It’s worth mentioning that on Windows and Linux, you can’t combine a custom title bar with a menu bar. When you create a custom title bar, you’re replacing the native one completely, which means you also lose the integrated native menu bar.

如果你同时需要自定义的标题栏和菜单栏,你可以使用 Flutter 进行实现,类似于自定义的上下文菜单。

If you need both a custom title bar and a menu bar, you can achieve that by implementing it in Flutter, similar to a custom context menu.

拖放(拖动和放置)

Drag and drop

拖放是基于触摸和指针的交互的一项核心。虽然这两种交互类型都需要拖放,但是在滑动整个包含可拖拽元素的列表时,仍然需要考虑其中的差异。

One of the core interactions for both touch-based and pointer-based inputs is drag and drop. Although this interaction is expected for both types of input, there are important differences to think about when it comes to scrolling lists of draggable items.

一般来说,触屏用户希望看到可拖动的手柄,以区分拖动和滚动的范围,或者通过长按操作来进行拖动。这是由于滑动和拖动操作都是由一个触摸点完成的。

Generally speaking, touch users expect to see drag handles to differentiate draggable areas from scrollable ones, or alternatively, to initiate a drag by using a long press gesture. This is because scrolling and dragging are both sharing a single finger for input.

鼠标用户有着不止一种输入方式。他们可以使用滚轮和滑动条进行滑动,这样便不再专门需要操作手柄进行指示操作。如果你使用过 macOS 的访达和 Windows 的资源管理器,你会看到它们在选中一个元素后,就可以开始拖动。

Mouse users have more input options. They can use a wheel or scrollbar to scroll, which generally eliminates the need for dedicated drag handles. If you look at the macOS Finder or Windows Explorer, you’ll see that they work this way: you just select an item and start dragging.

在 Flutter 中,你可以用多种方式实现拖放。但是我们不在本篇文章中讨论这个话题,以下是一些更高级的选项:

在 Flutter 中,你可以用多种方式实现拖放。但是我们不在本篇文章中讨论这个话题,以下是一些更高级的选项:

In Flutter, you can implement drag and drop in many ways. Discussing specific implementations is outside the scope of this article, but some high level options are:

自身做到熟悉基本的可用性原则

Educate yourself on basic usability principles

当然,这篇文章并不代表你仅需要考虑这些内容。针对平台设计的规范,会随着你适配的平台、设备外形和输入设备数量的增加而变得更为复杂。

Of course, this page doesn’t constitute an exhaustive list of the things you might consider. The more operating systems, form factors, and input devices you support, the more difficult it becomes to spec out every permutation in design.

作为开发人员,你应当花一些时间学习基本的可用性原则,帮助你做出更好的决策,减少由设计细节带来的返工时间消耗,从而提升自己的生产力,产出更好的结果。

Taking time to learn basic usability principles as a developer empowers you to make better decisions, reduces back-and-forth iterations with design during production, and results in improved productivity with better outcomes.

你可以从下列的资源开始学习:

Here are some resources to get you started:

深入理解 Flutter 布局约束

Hero image from the article

我们会经常听到一些开发者在学习 Flutter 时的疑惑:为什么我设置了 width:100,但是看上去却不是 100 像素宽呢。(注意,本文中的“像素”均指的是逻辑像素)通常你会回答,将这个 Widget 放进 Center 中,对吧?

When someone learning Flutter asks you why some widget with width:100 isn’t 100 pixels wide, the default answer is to tell them to put that widget inside of a Center, right?

别这么干。

Don’t do that.

如果你这样做了,他们会不断找你询问这样的问题:为什么 FittedBox 又不起作用了?为什么 Column 又溢出边界,亦或是 IntrinsicWidth 应该做什么。

If you do, they’ll come back again and again, asking why some FittedBox isn’t working, why that Column is overflowing, or what IntrinsicWidth is supposed to be doing.

其实我们首先应该做的,是告诉他们 Flutter 的布局方式与 HTML 的布局差异相当大(这些开发者很可能是 Web 开发),然后要让他们熟记这条规则:

Instead, first tell them that Flutter layout is very different from HTML layout (which is probably where they’re coming from), and then make them memorize the following rule:

首先,上层 widget 向下层 widget 传递约束条件;
然后,下层 widget 向上层 widget 传递大小信息。
最后,上层 widget 决定下层 widget 的位置。
Constraints go down. Sizes go up. Parent sets position.

如果我们在开发时无法熟练运用这条规则,在布局时就不能完全理解其原理,所以越早掌握这条规则越好!

Flutter layout can’t really be understood without knowing this rule, so Flutter developers should learn it early on.

更多细节:

In more detail:

例如,如果一个 widget 中包含了一个具有 padding 的 Column,并且要对 Column 的子 widget 进行如下的布局:

For example, if a composed widget contains a column with some padding, and wants to lay out its two children as follows:

Visual layout

那么谈判将会像这样:

The negotiation goes something like this:

Widget: “嘿!我的父级。我的约束是多少?”

Widget: “Hey parent, what are my constraints?”

Parent: “你的宽度必须在 80300 像素之间,高度必须在 3085 之间。”

Parent: “You must be from 80 to 300 pixels wide, and 30 to 85 tall.”

Widget: “嗯…我想要 5 个像素的内边距,这样我的子级能最多拥有 290 个像素宽度和 75 个像素高度。”

Widget: “Hmmm, since I want to have 5 pixels of padding, then my children can have at most 290 pixels of width and 75 pixels of height.”

Widget: “嘿,我的第一个子级,你的宽度必须要在 0290,长度在 075 之间。”

Widget: “Hey first child, You must be from 0 to 290 pixels wide, and 0 to 75 tall.”

First child: “OK,那我想要 290 像素的宽度,20 个像素的长度。”

First child: “OK, then I wish to be 290 pixels wide, and 20 pixels tall.”

Widget: “嗯…由于我想要将我的第二个子级放在第一个子级下面,所以我们仅剩 55 个像素的高度给第二个子级了。”

Widget: “Hmmm, since I want to put my second child below the first one, this leaves only 55 pixels of height for my second child.”

Widget: “嘿,我的第二个子级,你的宽度必须要在 0290,长度在 055 之间。”

Widget: “Hey second child, You must be from 0 to 290 wide, and 0 to 55 tall.”

Second child: “OK,那我想要 140 像素的宽度,30 个像素的长度。”

Second child: “OK, I wish to be 140 pixels wide, and 30 pixels tall.”

Widget: “很好。我的第一个子级将被放在 x: 5 & y: 5 的位置,而我的第二个子级将在 x: 80 & y: 25 的位置。”

Widget: “Very well. My first child has position x: 5 and y: 5, and my second child has x: 80 and y: 25.”

Widget: “嘿,我的父级,我决定我的大小为 300 像素宽度,60 像素高度。”

Widget: “Hey parent, I’ve decided that my size is going to be 300 pixels wide, and 60 pixels tall.”

限制

Limitations

正如上述所介绍的布局规则中所说的那样, Flutter 的布局引擎有一些重要限制:

As a result of the layout rule mentioned above, Flutter’s layout engine has a few important limitations:

样例

Examples

下面的示例由 DartPad 提供,具有良好的交互体验。使用下面水平滚动条的编号切换 29 个不同的示例。

For an interactive experience, use the following DartPad. Use the numbered horizontal scrolling bar to switch between 29 different examples.

import 'package:flutter/material.dart';

void main() => runApp(const HomePage());

const red = Colors.red;
const green = Colors.green;
const blue = Colors.blue;
const big = TextStyle(fontSize: 30);

//////////////////////////////////////////////////

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const FlutterLayoutArticle([
      Example1(),
      Example2(),
      Example3(),
      Example4(),
      Example5(),
      Example6(),
      Example7(),
      Example8(),
      Example9(),
      Example10(),
      Example11(),
      Example12(),
      Example13(),
      Example14(),
      Example15(),
      Example16(),
      Example17(),
      Example18(),
      Example19(),
      Example20(),
      Example21(),
      Example22(),
      Example23(),
      Example24(),
      Example25(),
      Example26(),
      Example27(),
      Example28(),
      Example29(),
    ]);
  }
}

//////////////////////////////////////////////////

abstract class Example extends StatelessWidget {
  const Example({Key? key}) : super(key: key);

  String get code;

  String get explanation;
}

//////////////////////////////////////////////////

class FlutterLayoutArticle extends StatefulWidget {
  const FlutterLayoutArticle(
    this.examples, {
    Key? key,
  }) : super(key: key);

  final List<Example> examples;

  @override
  _FlutterLayoutArticleState createState() => _FlutterLayoutArticleState();
}

//////////////////////////////////////////////////

class _FlutterLayoutArticleState extends State<FlutterLayoutArticle> {
  late int count;
  late Widget example;
  late String code;
  late String explanation;

  @override
  void initState() {
    count = 1;
    code = const Example1().code;
    explanation = const Example1().explanation;

    super.initState();
  }

  @override
  void didUpdateWidget(FlutterLayoutArticle oldWidget) {
    super.didUpdateWidget(oldWidget);
    var example = widget.examples[count - 1];
    code = example.code;
    explanation = example.explanation;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Layout Article',
      home: SafeArea(
        child: Material(
          color: Colors.black,
          child: FittedBox(
            child: Container(
              width: 400,
              height: 670,
              color: const Color(0xFFCCCCCC),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Expanded(
                      child: ConstrainedBox(
                          constraints: const BoxConstraints.tightFor(
                              width: double.infinity, height: double.infinity),
                          child: widget.examples[count - 1])),
                  Container(
                    height: 50,
                    width: double.infinity,
                    color: Colors.black,
                    child: SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          for (int i = 0; i < widget.examples.length; i++)
                            Container(
                              width: 58,
                              padding:
                                  const EdgeInsets.only(left: 4.0, right: 4.0),
                              child: button(i + 1),
                            ),
                        ],
                      ),
                    ),
                  ),
                  Container(
                    child: Scrollbar(
                      child: SingleChildScrollView(
                        key: ValueKey(count),
                        child: Padding(
                          padding: const EdgeInsets.all(10.0),
                          child: Column(
                            children: [
                              Center(child: Text(code)),
                              const SizedBox(height: 15),
                              Text(
                                explanation,
                                style: TextStyle(
                                    color: Colors.blue[900],
                                    fontStyle: FontStyle.italic),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                    height: 273,
                    color: Colors.grey[50],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget button(int exampleNumber) {
    return Button(
      key: ValueKey('button$exampleNumber'),
      isSelected: count == exampleNumber,
      exampleNumber: exampleNumber,
      onPressed: () {
        showExample(
          exampleNumber,
          widget.examples[exampleNumber - 1].code,
          widget.examples[exampleNumber - 1].explanation,
        );
      },
    );
  }

  void showExample(int exampleNumber, String code, String explanation) {
    setState(() {
      count = exampleNumber;
      this.code = code;
      this.explanation = explanation;
    });
  }
}

//////////////////////////////////////////////////

class Button extends StatelessWidget {
  final bool isSelected;
  final int exampleNumber;
  final VoidCallback onPressed;

  const Button({
    required Key key,
    required this.isSelected,
    required this.exampleNumber,
    required this.onPressed,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: TextButton.styleFrom(
        primary: Colors.white,
        backgroundColor: isSelected ? Colors.grey : Colors.grey[800],
      ),
      child: Text(exampleNumber.toString()),
      onPressed: () {
        Scrollable.ensureVisible(
          context,
          duration: const Duration(milliseconds: 350),
          curve: Curves.easeOut,
          alignment: 0.5,
        );
        onPressed();
      },
    );
  }
}
//////////////////////////////////////////////////

class Example1 extends Example {
  const Example1({Key? key}) : super(key: key);

  @override
  final code = 'Container(color: red)';

  @override
  final explanation = 'The screen is the parent of the Container, '
      'and it forces the Container to be exactly the same size as the screen.'
      '\n\n'
      'So the Container fills the screen and paints it red.';

  @override
  Widget build(BuildContext context) {
    return Container(color: red);
  }
}

//////////////////////////////////////////////////

class Example2 extends Example {
  const Example2({Key? key}) : super(key: key);

  @override
  final code = 'Container(width: 100, height: 100, color: red)';
  @override
  final String explanation =
      'The red Container wants to be 100x100, but it can\'t, '
      'because the screen forces it to be exactly the same size as the screen.'
      '\n\n'
      'So the Container fills the screen.';

  @override
  Widget build(BuildContext context) {
    return Container(width: 100, height: 100, color: red);
  }
}

//////////////////////////////////////////////////

class Example3 extends Example {
  const Example3({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: Container(width: 100, height: 100, color: red))';
  @override
  final String explanation =
      'The screen forces the Center to be exactly the same size as the screen,'
      'so the Center fills the screen.'
      '\n\n'
      'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
      'Now the Container can indeed be 100x100.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(width: 100, height: 100, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example4 extends Example {
  const Example4({Key? key}) : super(key: key);

  @override
  final code = 'Align(\n'
      '   alignment: Alignment.bottomRight,\n'
      '   child: Container(width: 100, height: 100, color: red))';
  @override
  final String explanation =
      'This is different from the previous example in that it uses Align instead of Center.'
      '\n\n'
      'Align also tells the Container that it can be any size it wants, but if there is empty space it won\'t center the Container. '
      'Instead, it aligns the Container to the bottom-right of the available space.';

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomRight,
      child: Container(width: 100, height: 100, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example5 extends Example {
  const Example5({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: Container(\n'
      '              color: red,\n'
      '              width: double.infinity,\n'
      '              height: double.infinity))';
  @override
  final String explanation =
      'The screen forces the Center to be exactly the same size as the screen,'
      'so the Center fills the screen.'
      '\n\n'
      'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
      'The Container wants to be of infinite size, but since it can\'t be bigger than the screen, it just fills the screen.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          width: double.infinity, height: double.infinity, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example6 extends Example {
  const Example6({Key? key}) : super(key: key);

  @override
  final code = 'Center(child: Container(color: red))';
  @override
  final String explanation =
      'The screen forces the Center to be exactly the same size as the screen,'
      'so the Center fills the screen.'
      '\n\n'
      'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
      '\n\n'
      'Since the Container has no child and no fixed size, it decides it wants to be as big as possible, so it fills the whole screen.'
      '\n\n'
      'But why does the Container decide that? '
      'Simply because that\'s a design decision by those who created the Container widget. '
      'It could have been created differently, and you have to read the Container documentation to understand how it behaves, depending on the circumstances. ';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example7 extends Example {
  const Example7({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: Container(color: red\n'
      '      child: Container(color: green, width: 30, height: 30)))';
  @override
  final String explanation =
      'The screen forces the Center to be exactly the same size as the screen,'
      'so the Center fills the screen.'
      '\n\n'
      'The Center tells the red Container that it can be any size it wants, but not bigger than the screen.'
      'Since the red Container has no size but has a child, it decides it wants to be the same size as its child.'
      '\n\n'
      'The red Container tells its child that it can be any size it wants, but not bigger than the screen.'
      '\n\n'
      'The child is a green Container that wants to be 30x30.'
      '\n\n'
      'Since the red `Container` has no size but has a child, it decides it wants to be the same size as its child. '
      'The red color isn\'t visible, since the green Container entirely covers all of the red Container.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: red,
        child: Container(color: green, width: 30, height: 30),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example8 extends Example {
  const Example8({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: Container(color: red\n'
      '      padding: const EdgeInsets.all(20.0),\n'
      '      child: Container(color: green, width: 30, height: 30)))';
  @override
  final String explanation =
      'The red Container sizes itself to its children size, but it takes its own padding into consideration. '
      'So it is also 30x30 plus padding. '
      'The red color is visible because of the padding, and the green Container has the same size as in the previous example.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: const EdgeInsets.all(20.0),
        color: red,
        child: Container(color: green, width: 30, height: 30),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example9 extends Example {
  const Example9({Key? key}) : super(key: key);

  @override
  final code = 'ConstrainedBox(\n'
      '   constraints: BoxConstraints(\n'
      '              minWidth: 70, minHeight: 70,\n'
      '              maxWidth: 150, maxHeight: 150),\n'
      '      child: Container(color: red, width: 10, height: 10)))';
  @override
  final String explanation =
      'You might guess that the Container has to be between 70 and 150 pixels, but you would be wrong. '
      'The ConstrainedBox only imposes ADDITIONAL constraints from those it receives from its parent.'
      '\n\n'
      'Here, the screen forces the ConstrainedBox to be exactly the same size as the screen, '
      'so it tells its child Container to also assume the size of the screen, '
      'thus ignoring its \'constraints\' parameter.';

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(
        minWidth: 70,
        minHeight: 70,
        maxWidth: 150,
        maxHeight: 150,
      ),
      child: Container(color: red, width: 10, height: 10),
    );
  }
}

//////////////////////////////////////////////////

class Example10 extends Example {
  const Example10({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 10, height: 10))))';
  @override
  final String explanation =
      'Now, Center allows ConstrainedBox to be any size up to the screen size.'
      '\n\n'
      'The ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child.'
      '\n\n'
      'The Container must be between 70 and 150 pixels. It wants to have 10 pixels, so it will end up having 70 (the MINIMUM).';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 10, height: 10),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example11 extends Example {
  const Example11({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 1000, height: 1000))))';
  @override
  final String explanation =
      'Center allows ConstrainedBox to be any size up to the screen size.'
      'The ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child'
      '\n\n'
      'The Container must be between 70 and 150 pixels. It wants to have 1000 pixels, so it ends up having 150 (the MAXIMUM).';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 1000, height: 1000),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example12 extends Example {
  const Example12({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 100, height: 100))))';
  @override
  final String explanation =
      'Center allows ConstrainedBox to be any size up to the screen size.'
      'ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child.'
      '\n\n'
      'The Container must be between 70 and 150 pixels. It wants to have 100 pixels, and that\'s the size it has, since that\'s between 70 and 150.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 100, height: 100),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example13 extends Example {
  const Example13({Key? key}) : super(key: key);

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: red, width: 20, height: 50));';
  @override
  final String explanation =
      'The screen forces the UnconstrainedBox to be exactly the same size as the screen.'
      'However, the UnconstrainedBox lets its child Container be any size it wants.';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: red, width: 20, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example14 extends Example {
  const Example14({Key? key}) : super(key: key);

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: red, width: 4000, height: 50));';
  @override
  final String explanation =
      'The screen forces the UnconstrainedBox to be exactly the same size as the screen, '
      'and UnconstrainedBox lets its child Container be any size it wants.'
      '\n\n'
      'Unfortunately, in this case the Container has 4000 pixels of width and is too big to fit in the UnconstrainedBox, '
      'so the UnconstrainedBox displays the much dreaded "overflow warning".';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: red, width: 4000, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example15 extends Example {
  const Example15({Key? key}) : super(key: key);

  @override
  final code = 'OverflowBox(\n'
      '   minWidth: 0.0,'
      '   minHeight: 0.0,'
      '   maxWidth: double.infinity,'
      '   maxHeight: double.infinity,'
      '   child: Container(color: red, width: 4000, height: 50));';
  @override
  final String explanation =
      'The screen forces the OverflowBox to be exactly the same size as the screen, '
      'and OverflowBox lets its child Container be any size it wants.'
      '\n\n'
      'OverflowBox is similar to UnconstrainedBox, and the difference is that it won\'t display any warnings if the child doesn\'t fit the space.'
      '\n\n'
      'In this case the Container is 4000 pixels wide, and is too big to fit in the OverflowBox, '
      'but the OverflowBox simply shows as much as it can, with no warnings given.';

  @override
  Widget build(BuildContext context) {
    return OverflowBox(
      minWidth: 0.0,
      minHeight: 0.0,
      maxWidth: double.infinity,
      maxHeight: double.infinity,
      child: Container(color: red, width: 4000, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example16 extends Example {
  const Example16({Key? key}) : super(key: key);

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: Colors.red, width: double.infinity, height: 100));';
  @override
  final String explanation =
      'This won\'t render anything, and you\'ll see an error in the console.'
      '\n\n'
      'The UnconstrainedBox lets its child be any size it wants, '
      'however its child is a Container with infinite size.'
      '\n\n'
      'Flutter can\'t render infinite sizes, so it throws an error with the following message: '
      '"BoxConstraints forces an infinite width."';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: Colors.red, width: double.infinity, height: 100),
    );
  }
}

//////////////////////////////////////////////////

class Example17 extends Example {
  const Example17({Key? key}) : super(key: key);

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: LimitedBox(maxWidth: 100,\n'
      '      child: Container(color: Colors.red,\n'
      '                       width: double.infinity, height: 100));';
  @override
  final String explanation = 'Here you won\'t get an error anymore, '
      'because when the LimitedBox is given an infinite size by the UnconstrainedBox, '
      'it passes a maximum width of 100 down to its child.'
      '\n\n'
      'If you swap the UnconstrainedBox for a Center widget, '
      'the LimitedBox won\'t apply its limit anymore (since its limit is only applied when it gets infinite constraints), '
      'and the width of the Container is allowed to grow past 100.'
      '\n\n'
      'This explains the difference between a LimitedBox and a ConstrainedBox.';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: LimitedBox(
        maxWidth: 100,
        child: Container(
          color: Colors.red,
          width: double.infinity,
          height: 100,
        ),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example18 extends Example {
  const Example18({Key? key}) : super(key: key);

  @override
  final code = 'FittedBox(\n'
      '   child: Text(\'Some Example Text.\'));';
  @override
  final String explanation =
      'The screen forces the FittedBox to be exactly the same size as the screen.'
      'The Text has some natural width (also called its intrinsic width) that depends on the amount of text, its font size, and so on.'
      '\n\n'
      'The FittedBox lets the Text be any size it wants, '
      'but after the Text tells its size to the FittedBox, '
      'the FittedBox scales the Text until it fills all of the available width.';

  @override
  Widget build(BuildContext context) {
    return const FittedBox(
      child: Text('Some Example Text.'),
    );
  }
}

//////////////////////////////////////////////////

class Example19 extends Example {
  const Example19({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: FittedBox(\n'
      '      child: Text(\'Some Example Text.\')));';
  @override
  final String explanation =
      'But what happens if you put the FittedBox inside of a Center widget? '
      'The Center lets the FittedBox be any size it wants, up to the screen size.'
      '\n\n'
      'The FittedBox then sizes itself to the Text, and lets the Text be any size it wants.'
      '\n\n'
      'Since both FittedBox and the Text have the same size, no scaling happens.';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: FittedBox(
        child: Text('Some Example Text.'),
      ),
    );
  }
}

////////////////////////////////////////////////////

class Example20 extends Example {
  const Example20({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: FittedBox(\n'
      '      child: Text(\'…\')));';
  @override
  final String explanation =
      'However, what happens if FittedBox is inside of a Center widget, but the Text is too large to fit the screen?'
      '\n\n'
      'FittedBox tries to size itself to the Text, but it can\'t be bigger than the screen. '
      'It then assumes the screen size, and resizes Text so that it fits the screen, too.';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: FittedBox(
        child: Text(
            'This is some very very very large text that is too big to fit a regular screen in a single line.'),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example21 extends Example {
  const Example21({Key? key}) : super(key: key);

  @override
  final code = 'Center(\n'
      '   child: Text(\'…\'));';
  @override
  final String explanation = 'If, however, you remove the FittedBox, '
      'the Text gets its maximum width from the screen, '
      'and breaks the line so that it fits the screen.';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text(
          'This is some very very very large text that is too big to fit a regular screen in a single line.'),
    );
  }
}

//////////////////////////////////////////////////

class Example22 extends Example {
  const Example22({Key? key}) : super(key: key);

  @override
  final code = 'FittedBox(\n'
      '   child: Container(\n'
      '      height: 20.0, width: double.infinity));';
  @override
  final String explanation =
      'FittedBox can only scale a widget that is BOUNDED (has non-infinite width and height).'
      'Otherwise, it won\'t render anything, and you\'ll see an error in the console.';

  @override
  Widget build(BuildContext context) {
    return FittedBox(
      child: Container(
        height: 20.0,
        width: double.infinity,
        color: Colors.red,
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example23 extends Example {
  const Example23({Key? key}) : super(key: key);

  @override
  final code = 'Row(children:[\n'
      '   Container(color: red, child: Text(\'Hello!\'))\n'
      '   Container(color: green, child: Text(\'Goodbye!\'))]';
  @override
  final String explanation =
      'The screen forces the Row to be exactly the same size as the screen.'
      '\n\n'
      'Just like an UnconstrainedBox, the Row won\'t impose any constraints onto its children, '
      'and instead lets them be any size they want.'
      '\n\n'
      'The Row then puts them side-by-side, and any extra space remains empty.';

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Container(color: red, child: const Text('Hello!', style: big)),
        Container(color: green, child: const Text('Goodbye!', style: big)),
      ],
    );
  }
}

//////////////////////////////////////////////////

class Example24 extends Example {
  const Example24({Key? key}) : super(key: key);

  @override
  final code = 'Row(children:[\n'
      '   Container(color: red, child: Text(\'…\'))\n'
      '   Container(color: green, child: Text(\'Goodbye!\'))]';
  @override
  final String explanation =
      'Since the Row won\'t impose any constraints onto its children, '
      'it\'s quite possible that the children might be too big to fit the available width of the Row.'
      'In this case, just like an UnconstrainedBox, the Row displays the "overflow warning".';

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Container(
          color: red,
          child: const Text(
            'This is a very long text that '
            'won\'t fit the line.',
            style: big,
          ),
        ),
        Container(color: green, child: const Text('Goodbye!', style: big)),
      ],
    );
  }
}

//////////////////////////////////////////////////

class Example25 extends Example {
  const Example25({Key? key}) : super(key: key);

  @override
  final code = 'Row(children:[\n'
      '   Expanded(\n'
      '       child: Container(color: red, child: Text(\'…\')))\n'
      '   Container(color: green, child: Text(\'Goodbye!\'))]';
  @override
  final String explanation =
      'When a Row\'s child is wrapped in an Expanded widget, the Row won\'t let this child define its own width anymore.'
      '\n\n'
      'Instead, it defines the Expanded width according to the other children, and only then the Expanded widget forces the original child to have the Expanded\'s width.'
      '\n\n'
      'In other words, once you use Expanded, the original child\'s width becomes irrelevant, and is ignored.';

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Center(
            child: Container(
              color: red,
              child: const Text(
                'This is a very long text that won\'t fit the line.',
                style: big,
              ),
            ),
          ),
        ),
        Container(color: green, child: const Text('Goodbye!', style: big)),
      ],
    );
  }
}

//////////////////////////////////////////////////

class Example26 extends Example {
  const Example26({Key? key}) : super(key: key);

  @override
  final code = 'Row(children:[\n'
      '   Expanded(\n'
      '       child: Container(color: red, child: Text(\'…\')))\n'
      '   Expanded(\n'
      '       child: Container(color: green, child: Text(\'Goodbye!\'))]';
  @override
  final String explanation =
      'If all of Row\'s children are wrapped in Expanded widgets, each Expanded has a size proportional to its flex parameter, '
      'and only then each Expanded widget forces its child to have the Expanded\'s width.'
      '\n\n'
      'In other words, Expanded ignores the preffered width of its children.';

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Container(
            color: red,
            child: const Text(
              'This is a very long text that won\'t fit the line.',
              style: big,
            ),
          ),
        ),
        Expanded(
          child: Container(
            color: green,
            child: const Text(
              'Goodbye!',
              style: big,
            ),
          ),
        ),
      ],
    );
  }
}

//////////////////////////////////////////////////

class Example27 extends Example {
  const Example27({Key? key}) : super(key: key);

  @override
  final code = 'Row(children:[\n'
      '   Flexible(\n'
      '       child: Container(color: red, child: Text(\'…\')))\n'
      '   Flexible(\n'
      '       child: Container(color: green, child: Text(\'Goodbye!\'))]';
  @override
  final String explanation =
      'The only difference if you use Flexible instead of Expanded, '
      'is that Flexible lets its child be SMALLER than the Flexible width, '
      'while Expanded forces its child to have the same width of the Expanded.'
      '\n\n'
      'But both Expanded and Flexible ignore their children\'s width when sizing themselves.'
      '\n\n'
      'This means that it\'s IMPOSSIBLE to expand Row children proportionally to their sizes. '
      'The Row either uses the exact child\'s width, or ignores it completely when you use Expanded or Flexible.';

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Flexible(
          child: Container(
            color: red,
            child: const Text(
              'This is a very long text that won\'t fit the line.',
              style: big,
            ),
          ),
        ),
        Flexible(
          child: Container(
            color: green,
            child: const Text(
              'Goodbye!',
              style: big,
            ),
          ),
        ),
      ],
    );
  }
}

//////////////////////////////////////////////////

class Example28 extends Example {
  const Example28({Key? key}) : super(key: key);

  @override
  final code = 'Scaffold(\n'
      '   body: Container(color: blue,\n'
      '   child: Column(\n'
      '      children: [\n'
      '         Text(\'Hello!\'),\n'
      '         Text(\'Goodbye!\')])))';

  @override
  final String explanation =
      'The screen forces the Scaffold to be exactly the same size as the screen,'
      'so the Scaffold fills the screen.'
      '\n\n'
      'The Scaffold tells the Container that it can be any size it wants, but not bigger than the screen.'
      '\n\n'
      'When a widget tells its child that it can be smaller than a certain size, '
      'we say the widget supplies "loose" constraints to its child. More on that later.';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: blue,
        child: Column(
          children: const [
            Text('Hello!'),
            Text('Goodbye!'),
          ],
        ),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example29 extends Example {
  const Example29({Key? key}) : super(key: key);

  @override
  final code = 'Scaffold(\n'
      '   body: Container(color: blue,\n'
      '   child: SizedBox.expand(\n'
      '      child: Column(\n'
      '         children: [\n'
      '            Text(\'Hello!\'),\n'
      '            Text(\'Goodbye!\')]))))';

  @override
  final String explanation =
      'If you want the Scaffold\'s child to be exactly the same size as the Scaffold itself, '
      'you can wrap its child with SizedBox.expand.'
      '\n\n'
      'When a widget tells its child that it must be of a certain size, '
      'we say the widget supplies "tight" constraints to its child. More on that later.';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox.expand(
        child: Container(
          color: blue,
          child: Column(
            children: const [
              Text('Hello!'),
              Text('Goodbye!'),
            ],
          ),
        ),
      ),
    );
  }
}

//////////////////////////////////////////////////

如果你愿意的话,你还可以在 这个 Github 仓库中 获取其代码。

If you prefer, you can grab the code from this GitHub repo.

以下各节将介绍这些示例。

The examples are explained in the following sections.

样例 1

Example 1

Example 1 layout

Container(color: red)

整个屏幕作为 Container 的父级,并且强制 Container 变成和屏幕一样的大小。

The screen is the parent of the Container, and it forces the Container to be exactly the same size as the screen.

所以这个 Container 充满了整个屏幕,并绘制成红色。

So the Container fills the screen and paints it red.

样例 2

Example 2

Example 2 layout

Container(width: 100, height: 100, color: red)

红色的 Container 想要变成 100 x 100 的大小,但是它无法变成,因为屏幕强制它变成和屏幕一样的大小。

The red Container wants to be 100 × 100, but it can’t, because the screen forces it to be exactly the same size as the screen.

所以 Container 充满了整个屏幕。

So the Container fills the screen.

样例 3

Example 3

Example 3 layout

Center(
  child: Container(width: 100, height: 100, color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满了屏幕。

The screen forces the Center to be exactly the same size as the screen, so the Center fills the screen.

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 可以真正变成 100 × 100 大小了。

The Center tells the Container that it can be any size it wants, but not bigger than the screen. Now the Container can indeed be 100 × 100.

样例 4

Example 4

Example 4 layout

Align(
  alignment: Alignment.bottomRight,
  child: Container(width: 100, height: 100, color: red),
)

与上一个样例不同的是,我们使用了 Align 而不是 Center

This is different from the previous example in that it uses Align instead of Center.

Align 同样也告诉 Container,你可以变成任意大小。但是,如果还留有空白空间的话,它不会居中 Container。相反,它将会在允许的空间内,把 Container 放在右下角(bottomRight)。

Align also tells the Container that it can be any size it wants, but if there is empty space it won’t center the Container. Instead, it aligns the container to the bottom-right of the available space.

样例 5

Example 5

Example 5 layout

Center(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

The screen forces the Center to be exactly the same size as the screen, so the Center fills the screen.

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 想要无限的大小,但是由于它不能比屏幕更大,所以就仅充满屏幕。

The Center tells the Container that it can be any size it wants, but not bigger than the screen. The Container wants to be of infinite size, but since it can’t be bigger than the screen, it just fills the screen.

样例 6

Example 6

Example 6 layout

Center(
  child: Container(color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

The screen forces the Center to be exactly the same size as the screen, so the Center fills the screen.

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。由于 Container 没有子级而且没有固定大小,所以它决定能有多大就有多大,所以它充满了整个屏幕。

The Center tells the Container that it can be any size it wants, but not bigger than the screen. Since the Container has no child and no fixed size, it decides it wants to be as big as possible, so it fills the whole screen.

但是,为什么 Container 做出了这个决定?非常简单,因为这个决定是由 Container widget 的创建者决定的。可能会因创造者而异,而且你还得阅读 Container 文档 来理解不同场景下它的行为。

But why does the Container decide that? Simply because that’s a design decision by those who created the Container widget. It could have been created differently, and you have to read the Container documentation to understand how it behaves, depending on the circumstances.

样例 7

Example 7

Example 7 layout

Center(
  child: Container(
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

The screen forces the Center to be exactly the same size as the screen, so the Center fills the screen.

然后 Center 告诉红色的 Container 可以变成任意大小,但是不能超出屏幕。由于 Container 没有固定大小但是有子级,所以它决定变成它 child 的大小。

The Center tells the red Container that it can be any size it wants, but not bigger than the screen. Since the red Container has no size but has a child, it decides it wants to be the same size as its child.

然后红色的 Container 告诉它的 child 可以变成任意大小,但是不能超出屏幕。

The red Container tells its child that it can be any size it wants, but not bigger than the screen.

而它的 child 是一个想要 30 × 30 大小绿色的 Container。由于红色的 Container 和其子级一样大,所以也变为 30 × 30。由于绿色的 Container 完全覆盖了红色 Container,所以你看不见它了。

The child is a green Container that wants to be 30 × 30. Given that the red Container sizes itself to the size of its child, it is also 30 × 30. The red color isn’t visible because the green Container entirely covers the red Container.

样例 8

Example 8

Example 8 layout

Center(
  child: Container(
    padding: const EdgeInsets.all(20.0),
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

红色 Container 变为其子级的大小,但是它将其 padding 带入了约束的计算中。所以它有一个 30 x 30 的外边距。由于这个外边距,所以现在你能看见红色了。而绿色的 Container 则还是和之前一样。

The red Container sizes itself to its children’s size, but it takes its own padding into consideration. So it is also 30 × 30 plus padding. The red color is visible because of the padding, and the green Container has the same size as in the previous example.

样例 9

Example 9

Example 9 layout

ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 70,
    minHeight: 70,
    maxWidth: 150,
    maxHeight: 150,
  ),
  child: Container(color: red, width: 10, height: 10),
)

你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样。 ConstrainedBox 仅对其从其父级接收到的约束下施加其他约束。

You might guess that the Container has to be between 70 and 150 pixels, but you would be wrong. The ConstrainedBox only imposes additional constraints from those it receives from its parent.

在这里,屏幕迫使 ConstrainedBox 与屏幕大小完全相同,因此它告诉其子 Widget 也以屏幕大小作为约束,从而忽略了其 constraints 参数带来的影响。

Here, the screen forces the ConstrainedBox to be exactly the same size as the screen, so it tells its child Container to also assume the size of the screen, thus ignoring its constraints parameter.

样例 10

Example 10

Example 10 layout

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Now, Center allows ConstrainedBox to be any size up to the screen size. The ConstrainedBox imposes additional constraints from its constraints parameter onto its child.

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 10 个像素大小,但最终获得了 70 个像素(最小为 70)。

The Container must be between 70 and 150 pixels. It wants to have 10 pixels, so it ends up having 70 (the minimum).

样例 11

Example 11

Example 11 layout

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 1000, height: 1000),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Center allows ConstrainedBox to be any size up to the screen size. The ConstrainedBox imposes additional constraints from its constraints parameter onto its child.

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 1000 个像素大小,但最终获得了 150 个像素(最大为 150)。

The Container must be between 70 and 150 pixels. It wants to have 1000 pixels, so it ends up having 150 (the maximum).

样例 12

Example 12

Example 12 layout

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 100, height: 100),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Center allows ConstrainedBox to be any size up to the screen size. The ConstrainedBox imposes additional constraints from its constraints parameter onto its child.

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 100 个像素大小,因为 100 介于 70 至 150 的范围内,所以最终获得了 100 个像素。

The Container must be between 70 and 150 pixels. It wants to have 100 pixels, and that’s the size it has, since that’s between 70 and 150.

样例 13

Example 13

Example 13 layout

UnconstrainedBox(
  child: Container(color: red, width: 20, height: 50),
)

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

The screen forces the UnconstrainedBox to be exactly the same size as the screen. However, the UnconstrainedBox lets its child Container be any size it wants.

样例 14

Example 14

Example 14 layout

UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

The screen forces the UnconstrainedBox to be exactly the same size as the screen, and UnconstrainedBox lets its child Container be any size it wants.

不幸的是,在这种情况下,容器的宽度为 4000 像素,这实在是太大,以至于无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 将显示溢出警告(overflow warning)。

Unfortunately, in this case the Container is 4000 pixels wide and is too big to fit in the UnconstrainedBox, so the UnconstrainedBox displays the much dreaded “overflow warning”.

样例 15

Example 15

Example 15 layout

OverflowBox(
  minWidth: 0.0,
  minHeight: 0.0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

屏幕强制 OverflowBox 变得和屏幕一样大,并且 OverflowBox 允许其子容器设置为任意大小。

The screen forces the OverflowBox to be exactly the same size as the screen, and OverflowBox lets its child Container be any size it wants.

OverflowBoxUnconstrainedBox 类似,但不同的是,如果其子级超出该空间,它将不会显示任何警告。

OverflowBox is similar to UnconstrainedBox; the difference is that it won’t display any warnings if the child doesn’t fit the space.

在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox 中,但是 OverflowBox 会全部显示,而不会发出警告。

In this case, the Container has 4000 pixels of width, and is too big to fit in the OverflowBox, but the OverflowBox simply shows as much as it can, with no warnings given.

样例 16

Example 16

Example 16 layout

UnconstrainedBox(
  child: Container(color: Colors.red, width: double.infinity, height: 100),
)

这将不会渲染任何东西,而且你能在控制台看到错误信息。

This won’t render anything, and you’ll see an error in the console.

UnconstrainedBox 让它的子级决定成为任何大小,但是其子级是一个具有无限大小的 Container

The UnconstrainedBox lets its child be any size it wants, however its child is a Container with infinite size.

Flutter 无法渲染无限大的东西,所以它抛出以下错误: BoxConstraints forces an infinite width.(盒子约束强制使用了无限的宽度)

Flutter can’t render infinite sizes, so it throws an error with the following message: BoxConstraints forces an infinite width.

样例 17

Example 17

Example 17 layout

UnconstrainedBox(
  child: LimitedBox(
    maxWidth: 100,
    child: Container(
      color: Colors.red,
      width: double.infinity,
      height: 100,
    ),
  ),
)

这次你就不会遇到报错了。 UnconstrainedBoxLimitedBox 一个无限的大小;但它向其子级传递了最大为 100 的约束。

Here you won’t get an error anymore, because when the LimitedBox is given an infinite size by the UnconstrainedBox; it passes a maximum width of 100 down to its child.

如果你将 UnconstrainedBox 替换为 Center,则LimitedBox 将不再应用其限制(因为其限制仅在获得无限约束时才适用),并且容器的宽度允许超过 100。

If you swap the UnconstrainedBox for a Center widget, the LimitedBox won’t apply its limit anymore (since its limit is only applied when it gets infinite constraints), and the width of the Container is allowed to grow past 100.

上面的样例解释了 LimitedBoxConstrainedBox 之间的区别。

This explains the difference between a LimitedBox and a ConstrainedBox.

样例 18

Example 18

Example 18 layout

const FittedBox(
  child: Text('Some Example Text.'),
)

屏幕强制 FittedBox 变得和屏幕一样大,而 Text 则是有一个自然宽度(也被称作 intrinsic 宽度),它取决于文本数量,字体大小等因素。

The screen forces the FittedBox to be exactly the same size as the screen. The Text has some natural width (also called its intrinsic width) that depends on the amount of text, its font size, and so on.

FittedBoxText 可以变为任意大小。但是在 Text 告诉 FittedBox 其大小后, FittedBox 缩放文本直到填满所有可用宽度。

The FittedBox lets the Text be any size it wants, but after the Text tells its size to the FittedBox, the FittedBox scales the Text until it fills all of the available width.

样例 19

Example 19

Example 19 layout

const Center(
  child: FittedBox(
    child: Text('Some Example Text.'),
  ),
)

但如果你将 FittedBox 放进 Center widget 中会发生什么? Center 将会让 FittedBox 能够变为任意大小,取决于屏幕大小。

But what happens if you put the FittedBox inside of a Center widget? The Center lets the FittedBox be any size it wants, up to the screen size.

FittedBox 然后会根据 Text 调整自己的大小,然后让 Text 可以变为所需的任意大小,由于二者具有同一大小,因此不会发生缩放。

The FittedBox then sizes itself to the Text, and lets the Text be any size it wants. Since both FittedBox and the Text have the same size, no scaling happens.

样例 20

Example 20

Example 20 layout

const Center(
  child: FittedBox(
    child: Text(
        'This is some very very very large text that is too big to fit a regular screen in a single line.'),
  ),
)

然而,如果 FittedBox 位于 Center 中,但 Text 太大而超出屏幕,会发生什么?

However, what happens if FittedBox is inside of a Center widget, but the Text is too large to fit the screen?

FittedBox 会尝试根据 Text 大小调整大小,但不能大于屏幕大小。然后假定屏幕大小,并调整 Text 的大小以使其也适合屏幕。

FittedBox tries to size itself to the Text, but it can’t be bigger than the screen. It then assumes the screen size, and resizes Text so that it fits the screen, too.

样例 21

Example 21

Example 21 layout

const Center(
  child: Text(
      'This is some very very very large text that is too big to fit a regular screen in a single line.'),
)

然而,如果你删除了 FittedBoxText 则会从屏幕上获取其最大宽度,并在合适的地方换行。

If, however, you remove the FittedBox, the Text gets its maximum width from the screen, and breaks the line so that it fits the screen.

样例 22

Example 22

Example 22 layout

FittedBox(
  child: Container(
    height: 20.0,
    width: double.infinity,
    color: Colors.red,
  ),
)

FittedBox 只能在有限制的宽高中对子 widget 进行缩放(宽度和高度不会变得无限大)。否则,它将无法渲染任何内容,并且你会在控制台中看到错误。

FittedBox can only scale a widget that is bounded (has non-infinite width and height). Otherwise, it won’t render anything, and you’ll see an error in the console.

样例 23

Example 23

Example 23 layout

Row(
  children: [
    Container(color: red, child: const Text('Hello!', style: big)),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

屏幕强制 Row 变得和屏幕一样大,所以 Row 充满屏幕。

The screen forces the Row to be exactly the same size as the screen.

UnconstrainedBox 一样, Row 也不会对其子代施加任何约束,而是让它们成为所需的任意大小。 Row 然后将它们并排放置,任何多余的空间都将保持空白。

Just like an UnconstrainedBox, the Row won’t impose any constraints onto its children, and instead lets them be any size they want. The Row then puts them side-by-side, and any extra space remains empty.

样例 24

Example 24

Example 24 layout

Row(
  children: [
    Container(
      color: red,
      child: const Text(
        'This is a very long text that '
        'won\'t fit the line.',
        style: big,
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

由于 Row 不会对其子级施加任何约束,因此它的 children 很有可能太大而超出 Row 的可用宽度。在这种情况下, Row 会和 UnconstrainedBox 一样显示溢出警告。

Since Row won’t impose any constraints onto its children, it’s quite possible that the children might be too big to fit the available width of the Row. In this case, just like an UnconstrainedBox, the Row displays the “overflow warning”.

样例 25

Example 25

Example 25 layout

Row(
  children: [
    Expanded(
      child: Center(
        child: Container(
          color: red,
          child: const Text(
            'This is a very long text that won\'t fit the line.',
            style: big,
          ),
        ),
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Row 的子级被包裹在了 Expanded widget 之后, Row 将不会再让其决定自身的宽度了。

When a Row’s child is wrapped in an Expanded widget, the Row won’t let this child define its own width anymore.

取而代之的是,Row 会根据所有 Expanded 的子级来计算其该有的宽度。

Instead, it defines the Expanded width according to the other children, and only then the Expanded widget forces the original child to have the Expanded’s width.

换句话说,一旦你使用 Expanded,子级自身的宽度就变得无关紧要,直接会被忽略掉。

In other words, once you use Expanded, the original child’s width becomes irrelevant, and is ignored.

样例 26

Example 26

Example 26 layout

Row(
  children: [
    Expanded(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Expanded(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

如果所有 Row 的子级都被包裹了 Expanded widget,每一个 Expanded 大小都会与其 flex 因子成比例,并且 Expanded widget 将会强制其子级具有与 Expanded 相同的宽度。

If all of Row’s children are wrapped in Expanded widgets, each Expanded has a size proportional to its flex parameter, and only then each Expanded widget forces its child to have the Expanded’s width.

换句话说,Expanded 忽略了其子 Widget 想要的宽度。

In other words, Expanded ignores the preferred width of its children.

样例 27

Example 27

Example 27 layout

Row(
  children: [
    Flexible(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Flexible(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

如果你使用 Flexible 而不是 Expanded 的话,唯一的区别是,Flexible 会让其子级具有与 Flexible 相同或者更小的宽度。而 Expanded 将会强制其子级具有和 Expanded 相同的宽度。但无论是 Expanded 还是 Flexible 在它们决定子级大小时都会忽略其宽度。

The only difference if you use Flexible instead of Expanded, is that Flexible lets its child have the same or smaller width than the Flexible itself, while Expanded forces its child to have the exact same width of the Expanded. But both Expanded and Flexible ignore their children’s width when sizing themselves.

样例 28

Example 28

Example 28 layout

Scaffold(
  body: Container(
    color: blue,
    child: Column(
      children: const [
        Text('Hello!'),
        Text('Goodbye!'),
      ],
    ),
  ),
)

屏幕强制 Scaffold 变得和屏幕一样大,所以 Scaffold 充满屏幕。然后 Scaffold 告诉 Container 可以变为任意大小,但不能超出屏幕。

The screen forces the Scaffold to be exactly the same size as the screen, so the Scaffold fills the screen. The Scaffold tells the Container that it can be any size it wants, but not bigger than the screen.

样例 29

Example 29

Example 29 layout

Scaffold(
  body: SizedBox.expand(
    child: Container(
      color: blue,
      child: Column(
        children: const [
          Text('Hello!'),
          Text('Goodbye!'),
        ],
      ),
    ),
  ),
)

如果你想要 Scaffold 的子级变得和 Scaffold 本身一样大的话,你可以将这个子级外包裹一个 SizedBox.expand

If you want the Scaffold’s child to be exactly the same size as the Scaffold itself, you can wrap its child with SizedBox.expand.

严格约束(Tight) vs 宽松约束(loose)

Tight vs. loose constraints

以后你经常会听到一些约束为严格约束或宽松约束,你花点时间来弄明白它们是值得的。

It’s very common to hear that some constraint is “tight” or “loose”, so it’s worth knowing what that means.

严格约束给你了一种获得确切大小的选择。换句话来说就是,它的最大/最小宽度是一致的,高度也一样。

A tight constraint offers a single possibility, an exact size. In other words, a tight constraint has its maximum width equal to its minimum width; and has its maximum height equal to its minimum height.

如果你到 Flutter 的 box.dart 文件中搜索 BoxConstraints 构造器,你会发现以下内容:

If you go to Flutter’s box.dart file and search for the BoxConstraints constructors, you’ll find the following:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

如果你重新阅读 样例 2,它告诉我们屏幕强制 Container 变得和屏幕一样大。为何屏幕能够做到这一点,原因就是给 Container 传递了严格约束。

If you revisit Example 2 above, it tells us that the screen forces the red Container to be exactly the same size as the screen. The screen does that, of course, by passing tight constraints to the Container.

一个 宽松 约束,换句话来说就是设置了最大宽度/高度,但是让允许其子 widget 获得比它更小的任意大小。换句话来说,宽松约束的最小宽度/高度为 0

A loose constraint, on the other hand, sets the maximum width and height, but lets the widget be as small as it wants. In other words, a loose constraint has a minimum width and height both equal to zero:

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

如果你访问 样例 3,它将会告诉我们 Center 让红色的 Container 变得更小,但是不能超出屏幕。Center 能够做到这一点的原因就在于给 Container 的是一个宽松约束。总的来说,Center 起的作用就是从其父级(屏幕)那里获得的严格约束,为其子级(Container)转换为宽松约束。

If you revisit Example 3, it tells us that the Center lets the red Container be smaller, but not bigger than the screen. The Center does that, of course, by passing loose constraints to the Container. Ultimately, the Center’s very purpose is to transform the tight constraints it got from its parent (the screen) to loose constraints for its child (the Container).

了解如何为特定 widget 制定布局规则

Learning the layout rules for specific widgets

掌握通用布局是非常重要的,但这还不够。

Knowing the general layout rule is necessary, but it’s not enough.

应用一般规则时,每个 widget 都具有很大的自由度,所以没有办法只看 widget 的名称就知道可能它长什么样。

Each widget has a lot of freedom when applying the general rule, so there is no way of knowing what it will do by just reading the widget’s name.

如果你尝试推测,可能就会猜错。除非你已阅读 widget 的文档或研究了其源代码,否则你无法确切知道 widget 的行为。

If you try to guess, you’ll probably guess wrong. You can’t know exactly how a widget behaves unless you’ve read its documentation, or studied its source-code.

布局源代码通常很复杂,因此阅读文档是更好的选择。但是当你在研究布局源代码时,可以使用 IDE 的导航功能轻松找到它。

The layout source-code is usually complex, so it’s probably better to just read the documentation. However, if you decide to study the layout source-code, you can easily find it by using the navigating capabilities of your IDE.

下面是一个例子:

Here is an example:

A goodbye layout


本文作者:Marcelo Glasberg

Article by Marcelo Glasberg

Marcelo Glasberg 最初在 Medium 发表 Flutter: The Advanced Layout Rule Even Beginners Must Know 本文。我们十分喜欢这篇文章,在征得他的允许后发布在 flutter.dev。再次感谢你,Marcelo! 你可以找到 GitHub 以及 pub.dev 找到 Marcelo。

Marcelo originally published this content as Flutter: The Advanced Layout Rule Even Beginners Must Know on Medium. We loved it and asked that he allow us to publish in on docs.flutter.dev, to which he graciously agreed. Thanks, Marcelo! You can find Marcelo on GitHub and pub.dev.

同时,还要感谢 Simon Lightfoot 创造了本文的标题图片。

Also, thanks to Simon Lightfoot for creating the header image at the top of the article.

处理边界约束 (Box constraints) 的问题

目录

Flutter 中的 widget 由在其底层的 RenderBox 对象渲染而成。渲染框由其父级 widget 给出约束,并根据这些约束调整自身尺寸大小。约束是由最小宽度、最大宽度、最小高度、最大高度四个方面构成;尺寸大小则由特定的宽度和高度两个方面构成。

In Flutter, widgets are rendered by their underlying RenderBox objects. Render boxes are given constraints by their parent, and size themselves within those constraints. Constraints consist of minimum and maximum widths and heights; sizes consist of a specific width and height.

一般来说,从如何处理约束的角度来看,有以下三种类型的渲染框:

Generally, there are three kinds of boxes, in terms of how they handle their constraints:

对于一些诸如 Container 的 widget,其尺寸会因构造方法的参数而异,就 Container 来说,它默认是尽可能大的,而一旦给它一个特定的宽度,那么它就会遵照这个特定的宽度来调整自身尺寸。

Some widgets, for example Container, vary from type to type based on their constructor arguments. In the case of Container, it defaults to trying to be as big as possible, but if you give it a width, for instance, it tries to honor that and be that particular size.

其它一些像 Row and Column (flex boxes)这样的 widget ,其尺寸会因给定的约束而异,具体细节见后文 “Flex” 部分;

Others, for example Row and (flex boxes) vary based on the constraints they are given, as described below in the “Flex” section.

约束有时是”紧密的”,这意味着这些约束严格地限定了渲染框在定夺自身尺寸方面的空间(例如:当约束的最小宽度和最大宽度相同时,这种情况下,我们称这个约束有紧密宽度),这方面的主要例子是 App Widget,它是 RenderView 类里面的一个 widget: 由应用程序的 build 函数返回的子 widget 渲染框被指定了一个约束,该约束强制 App Widget 精确填充应用程序的内容区域(通常是整个屏幕)。 Flutter 中的许多渲染框,特别是那些只包含单个 widget 的渲染框,都会将自身的约束传递给他们的子级 widget。这意味着如果你在应用程序渲染树的根部嵌套了一些渲染框,这些框将会在受到约束的影响下相互适应彼此。

The constraints are sometimes “tight”, meaning that they leave no room for the render box to decide on a size (for example, if the minimum and maximum width are the same, it is said to have a tight width). An example of this is the App widget, which is contained by the RenderView class: the box used by the child returned by the application’s build function is given a constraint that forces it to exactly fill the application’s content area (typically, the entire screen). Many of the boxes in Flutter, especially those that just take a single child, pass their constraint on to their children. This means that if you nest a bunch of boxes inside each other at the root of your application’s render tree, they’ll all exactly fit in each other, forced by these tight constraints.

有些渲染框放松了约束,即:约束中只有最大宽度,最大高度,但没有最小宽度,最小高度,例如 Center

Some boxes loosen the constraints, meaning the maximum is maintained but the minimum is removed. For example, Center.

无边界约束

Unbounded constraints

在某些情况下,传递给框的约束是 无边界 的或无限的。这意味着约束的最大宽度或最大高度为 double.infinity

In certain situations, the constraint that is given to a box is unbounded, or infinite. This means that either the maximum width or the maximum height is set to double.infinity.

当传递无边界约束给类型为尽可能大的框时会失效,在 debug 模式下,则会抛出异常,该异常信息会把你引导到本页面。

A box that tries to be as big as possible won’t function usefully when given an unbounded constraint and, in debug mode, such a combination throws an exception that points to this file.

渲染框具有无边界约束的最常见情况是:当其被置于 flex boxes (RowColumn)内以及 可滚动区域(ListView 和其它 ScrollView 的子类)内时。

The most common cases where a render box finds itself with unbounded constraints are within flex boxes (Row and Column), and within scrollable regions (ListView and other ScrollView subclasses).

特别是 ListView 会试图扩展以适应其交叉方向可用空间 (比如说,如果它是一个垂直滚动块,它将试图扩充到与其父 widget 一样宽)。如果让垂直滚动的 ListView 嵌套在水平滚动的 ListView 内,那么被嵌套在里面的垂直滚动的 ListView 将会试图尽可能宽,直到无限宽,因为将其嵌套的是一个水平滚动的ListView,它可以在水平方向上一直滚动。

In particular, ListView tries to expand to fit the space available in its cross-direction (for example, if it’s a vertically-scrolling block, it tries to be as wide as its parent). If you nest a vertically scrolling ListView inside a horizontally scrolling ListView, the inner one tries to be as wide as possible, which is infinitely wide, since the outer one is scrollable in that direction.

Flex

Flex 框本身(RowColumn)的行为会有所不同,这取决于其在给定方向上是处于有边界约束还是无边界约束。

Flex boxes themselves (Row and Column) behave differently based on whether they are in bounded constraints or unbounded constraints in their given direction.

在有边界约束条件下,它们在给定方向上会尽可能大。

In bounded constraints, they try to be as big as possible in that direction.

在无边界约束条件下,它们试图让其子 widget 自适应这个给定的方向。在这种情况下,不能将子 widget 的flex属性设置为 0(默认值)以外的任何值。这意味着在 widget 库中,当一个 flex 框嵌套在另外一个 flex 框或者嵌套在可滚动区域内时,不能使用 Expanded。如果这样做了,就会收到异常,该异常信息会把你引导到本页面。

In unbounded constraints, they try to fit their children in that direction. In this case, you cannot set flex on the children to anything other than 0. In the widget library, this means that you cannot use Expanded when the flex box is inside another flex box or inside a scrollable. If you do, you’ll get an exception message pointing you at this document.

交叉 方向上,如 Column(垂直的 flex)的宽度和 Row(水平的 flex)的高度,它们必将不能是无界的,否则它们将无法合理地对齐它们的子 widget。

In the cross direction, for example, in the width for Column (vertical flex) or in the height for Row (horizontal flex), they must never be unbounded, otherwise they would not be able to reasonably align their children.

为你的 Flutter 应用加入交互体验

目录

如何修改您的应用程序以使其对用户输入做出反应?在本教程中,您将为仅包含非交互式 widget 的应用程序添加交互性。具体来说,您将通过创建一个管理两个无状态 widget 的自定义有状态 widget,修改一个图标实现使其可点击。

How do you modify your app to make it react to user input? In this tutorial, you’ll add interactivity to an app that contains only non-interactive widgets. Specifically, you’ll modify an icon to make it tappable by creating a custom stateful widget that manages two stateless widgets.

构建布局教程 中展示了如何构建下面截图所示的布局。

The building layouts tutorial showed you how to create the layout for the following screenshot.

The layout tutorial app
The layout tutorial app

当应用第一次启动时,这个星形图标是实心红色,表明这个湖以前已经被收藏过了。星号旁边的数字表示 41 个人已经收藏了此湖。完成本教程后,点击星形图标将取消收藏状态,然后用轮廓线的星形图标代替实心的,并减少计数。再次点击会重新收藏,并增加计数。

When the app first launches, the star is solid red, indicating that this lake has previously been favorited. The number next to the star indicates that 41 people have favorited this lake. After completing this tutorial, tapping the star removes its favorited status, replacing the solid star with an outline and decreasing the count. Tapping again favorites the lake, drawing a solid star and increasing the count.

The custom widget you'll create

为了实现这个,您将创建一个包含星形图标和计数的自定义 widget,它们都是 widget。因为点击星形图标会更改这两个 widget 的状态,所以同一个 widget 应该同时管理这两个 widget。

To accomplish this, you’ll create a single custom widget that includes both the star and the count, which are themselves widgets. Tapping the star changes state for both widgets, so the same widget should manage both.

您可以直接查看 第二步: 创建 StatefulWidget 的子类。如果您想尝试不同的管理状态方式,请跳至 状态管理

You can get right to touching the code in Step 2: Subclass StatefulWidget. If you want to try different ways of managing state, skip to Managing state.

有状态和无状态的 widgets

Stateful and stateless widgets

有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是 有状态的

A widget is either stateful or stateless. If a widget can change—when a user interacts with it, for example—it’s stateful.

无状态的 widget 自身无法改变。 IconIconButtonText 都是无状态 widget,它们都是 StatelessWidget 的子类。

A stateless widget never changes. Icon, IconButton, and Text are examples of stateless widgets. Stateless widgets subclass StatelessWidget.

有状态的 widget 自身是可动态改变的(基于State)。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。 CheckboxRadioSliderInkWellFormTextField 都是有状态 widget,它们都是 StatefulWidget 的子类。

A stateful widget is dynamic: for example, it can change its appearance in response to events triggered by user interactions or when it receives data. Checkbox, Radio, Slider, InkWell, Form, and TextField are examples of stateful widgets. Stateful widgets subclass StatefulWidget.

一个 widget 的状态保存在一个 State 对象中,它和 widget 的显示分离。 Widget 的状态是一些可以更改的值,如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时,State 对象调用 setState(),告诉框架去重绘 widget。

A widget’s state is stored in a State object, separating the widget’s state from its appearance. The state consists of values that can change, like a slider’s current value or whether a checkbox is checked. When the widget’s state changes, the state object calls setState(), telling the framework to redraw the widget.

创建一个有状态的 widget

Creating a stateful widget

在本节中,您将创建一个自定义有状态的 widget。您将使用一个自定义有状态 widget 来替换两个无状态 widget—— 红色实心星形图标和其旁边的数字计数—— 该 widget 用两个子 widget 管理一行 IconButtonText

In this section, you’ll create a custom stateful widget. You’ll replace two stateless widgets—the solid red star and the numeric count next to it—with a single custom stateful widget that manages a row with two children widgets: an IconButton and Text.

实现一个自定义的有状态 widget 需要创建两个类:

Implementing a custom stateful widget requires creating two classes:

这一节展示如何为 Lakes 应用程序构建一个名为 FavoriteWidget 的 StatefulWidget。第一步是选择如何管理 FavoriteWidget 的状态。

This section shows you how to build a stateful widget, called FavoriteWidget, for the lakes app. After setting up, your first step is choosing how state is managed for FavoriteWidget.

步骤 0: 开始

Step 0: Get ready

如果你已经在 构建布局教程(第 6 步) 中成功创建了应用程序,你可以跳过下面的部分。

If you’ve already built the app in the building layouts tutorial (step 6), skip to the next section.

  1. 确保你已经 设置 好了你的环境。

    Make sure you’ve set up your environment.

  2. 创建一个基础的「Hello world」Flutter 应用

    Create a basic “Hello World” Flutter app.

  3. 用 GitHub 上的 main.dart 替换 lib/main.dart 文件。

    Replace the lib/main.dart file with main.dart.

  4. 用 GitHub 上的 pubspec.yaml 替换 pubspec.yaml 文件。

    Replace the pubspec.yaml file with pubspec.yaml.

  5. 在你的工程中创建一个 images 文件夹,并添加 lake.jpg

    Create an images directory in your project, and add lake.jpg.

如果你有一个连接并可用的设备,或者你已经启动了 iOS 模拟器 或者 Android 模拟器 (Flutter 安装部分介绍过),你就可以开始了!

Once you have a connected and enabled device, or you’ve launched the iOS simulator (part of the Flutter install) or the Android emulator (part of the Android Studio install), you are good to go!

Step 1: 决定哪个对象管理 widget 的状态

Step 1: Decide which object manages the widget’s state

一个 widget 的状态可以通过多种方式进行管理,但在我们的示例中,widget 本身 ——FavoriteWidget—— 将管理自己的状态。在这个例子中,切换星形图标是一个独立的操作,不会影响父窗口 widget 或其他用户界面,因此该 widget 可以在内部处理它自己的状态。

A widget’s state can be managed in several ways, but in our example the widget itself, FavoriteWidget, will manage its own state. In this example, toggling the star is an isolated action that doesn’t affect the parent widget or the rest of the UI, so the widget can handle its state internally.

你可以在 状态管理 中了解更多关于 widget 和状态的分离以及如何管理状态的信息。

Learn more about the separation of widget and state, and how state might be managed, in Managing state.

Step 2: 创建 StatefulWidget 的子类

Step 2: Subclass StatefulWidget

FavoriteWidget 类管理自己的状态,因此它通过重写 createState() 来创建状态对象。框架会在构建 widget 时调用 createState()。在这个例子中,createState() 创建 _FavoriteWidgetState 的实例,您将在下一步中实现该实例。

The FavoriteWidget class manages its own state, so it overrides createState() to create a State object. The framework calls createState() when it wants to build the widget. In this example, createState() returns an instance of _FavoriteWidgetState, which you’ll implement in the next step.

lib/main.dart (FavoriteWidget)
class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({Key? key}) : super(key: key);

  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

Step 3: 创建 State 的子类

Step 3: Subclass State

_FavoriteWidgetState 类存储可变信息;可以在 widget 的生命周期内改变逻辑和内部状态。当应用第一次启动时,用户界面显示一个红色实心的星星形图标,表明该湖已经被收藏,并有 41 个「喜欢」。状态对象存储这些信息在 _isFavorited_favoriteCount 变量中。

The _FavoriteWidgetState class stores the mutable data that can change over the lifetime of the widget. When the app first launches, the UI displays a solid red star, indicating that the lake has “favorite” status, along with 41 likes. These values are stored in the _isFavorited and _favoriteCount fields:

lib/main.dart (_FavoriteWidgetState fields)
class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

  // ···
}

状态对象也定义了 build() 方法。这个 build() 方法创建一个包含红色 IconButtonText 的行。该 widget 使用 IconButton(而不是 Icon),因为它具有一个 onPressed 属性,该属性定义了处理点击的回调方法 (_toggleFavorite)。你将会在接下来的步骤中尝试定义它。

The class also defines a build() method, which creates a row containing a red IconButton, and Text. You use IconButton (instead of Icon) because it has an onPressed property that defines the callback function (_toggleFavorite) for handling a tap. You’ll define the callback function next.

lib/main.dart (_FavoriteWidgetState build)
class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.centerRight,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(
          width: 18,
          child: SizedBox(
            child: Text('$_favoriteCount'),
          ),
        ),
      ],
    );
  }
}

按下 IconButton 时会调用 _toggleFavorite() 方法,然后它会调用 setState()。调用 setState() 是至关重要的,因为这告诉框架, widget 的状态已经改变,应该重绘。 setState() 在如下两种状态中切换 UI:

The _toggleFavorite() method, which is called when the IconButton is pressed, calls setState(). Calling setState() is critical, because this tells the framework that the widget’s state has changed and that the widget should be redrawn. The function argument to setState() toggles the UI between these two states:

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

Step 4: 将有 stateful widget 插入 widget 树中

Step 4: Plug the stateful widget into the widget tree

将您自定义 stateful widget 在 build() 方法中添加到 widget 树中。首先,找到创建 IconText 的代码,并删除它,在相同的位置创建有状态的 widget:

Add your custom stateful widget to the widget tree in the app’s build() method. First, locate the code that creates the Icon and Text, and delete it. In the same location, create the stateful widget:

layout/lakes/{step6 → interactive}/lib/main.dart
@@ -10,2 +5,2 @@
10
5
  class MyApp extends StatelessWidget {
11
6
  const MyApp({Key? key}) : super(key: key);
@@ -40,11 +35,7 @@
40
35
  ],
41
36
  ),
42
37
  ),
43
- Icon(
44
- Icons.star,
45
- color: Colors.red[500],
46
- ),
47
- const Text('41'),
38
+ const FavoriteWidget(),
48
39
  ],
49
40
  ),
50
41
  );

就是这样!当您热重载应用后,星形图标就会响应点击了。

That’s it! When you hot reload the app, the star icon should now respond to taps.

有问题?

Problems?

如果您的代码无法运行,请在 IDE 中查找可能的错误。 调试 Flutter 应用程序 可能会有所帮助。如果仍然无法找到问题,请根据 GitHub 上的示例检查代码。

If you can’t get your code to run, look in your IDE for possible errors. Debugging Flutter apps might help. If you still can’t find the problem, check your code against the interactive lakes example on GitHub.

如果您仍有问题,可以咨询 社区 中的任何一位开发者。

If you still have questions, refer to any one of the developer community channels.


本页面的其余部分介绍了可以管理 widget 状态的几种方式,并列出了其他可用的可交互的 widget。

The rest of this page covers several ways a widget’s state can be managed, and lists other available interactive widgets.

状态管理

Managing state

谁管理着 stateful widget 的状态?widget 本身?父 widget?双方?另一个对象?答案是…… 这取决于实际情况。有几种有效的方法可以给你的 widget 加入交互。作为 widget 设计师,你可以基于你所期待的表现 widget 的方式来做决定。以下是一些管理状态的最常见的方法:

Who manages the stateful widget’s state? The widget itself? The parent widget? Both? Another object? The answer is… it depends. There are several valid ways to make your widget interactive. You, as the widget designer, make the decision based on how you expect your widget to be used. Here are the most common ways to manage state:

如何决定使用哪种管理方法?以下原则可以帮助您决定:

How do you decide which approach to use? The following principles should help you decide:

如果有疑问,首选是在父 widget 中管理状态。

If in doubt, start by managing state in the parent widget.

我们将通过创建三个简单示例来举例说明管理状态的不同方式: TapboxA、TapboxB 和 TapboxC。这些例子功能是相似的—— 每创建一个容器,当点击时,在绿色或灰色框之间切换。 _active 确定颜色:绿色为 true,灰色为 false。

We’ll give examples of the different ways of managing state by creating three simple examples: TapboxA, TapboxB, and TapboxC. The examples all work similarly—each creates a container that, when tapped, toggles between a green or grey box. The _active boolean determines the color: green for active or grey for inactive.

Active state Inactive state

这些示例使用 GestureDetector 捕获 Container 上的用户动作。

These examples use GestureDetector to capture activity on the Container.

widget 管理自己的状态

The widget manages its own state

有时,widget 在内部管理其状态是最好的。例如,当 ListView 的内容超过渲染框时, ListView 自动滚动。大多数使用 ListView 的开发人员不想管理 ListView 的滚动行为,因此 ListView 本身管理其滚动偏移量。

Sometimes it makes the most sense for the widget to manage its state internally. For example, ListView automatically scrolls when its content exceeds the render box. Most developers using ListView don’t want to manage ListView’s scrolling behavior, so ListView itself manages its scroll offset.

_TapboxAState 类:

The _TapboxAState class:

import 'package:flutter/material.dart';

// TapboxA manages its own state.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  const TapboxA({Key? key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo'),
        ),
        body: const Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}

父 widget 管理 widget 的 state

The parent widget manages the widget’s state

一般来说父 widget 管理状态并告诉其子 widget 何时更新通常是最合适的。例如,IconButton 允许您将图标视为可点按的按钮。 IconButton 是一个无状态 widget,因为我们认为父 widget 需要知道该按钮是否被点击来采取相应的处理。

Often it makes the most sense for the parent widget to manage the state and tell its child widget when to update. For example, IconButton allows you to treat an icon as a tappable button. IconButton is a stateless widget because we decided that the parent widget needs to know whether the button has been tapped, so it can take appropriate action.

在以下示例中,TapboxB 通过回调将其状态到其父类。由于 TapboxB 不管理任何状态,因此它继承自 StatelessWidget。

In the following example, TapboxB exports its state to its parent through a callback. Because TapboxB doesn’t manage any state, it subclasses StatelessWidget.

ParentWidgetState 类:

The ParentWidgetState class:

TapboxB 类:

The TapboxB class:

import 'package:flutter/material.dart';

// ParentWidget manages the state for TapboxB.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({Key? key}) : super(key: key);

  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  const TapboxB({
    Key? key,
    this.active = false,
    required this.onChanged,
  }) : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

混搭管理

A mix-and-match approach

对于一些 widget 来说,混搭管理的方法最合适的。在这种情况下,有状态的 widget 自己管理一些状态,同时父 widget 管理其他方面的状态。

For some widgets, a mix-and-match approach makes the most sense. In this scenario, the stateful widget manages some of the state, and the parent widget manages other aspects of the state.

TapboxC 示例中,点击时,盒子的周围会出现一个深绿色的边框。点击时,边框消失,盒子的颜色改变。 TapboxC 将其 _active 状态导出到其父 widget 中,但在内部管理其 _highlight 状态。这个例子有两个状态对象 _ParentWidgetState_TapboxCState

In the TapboxC example, on tap down, a dark green border appears around the box. On tap up, the border disappears and the box’s color changes. TapboxC exports its _active state to its parent but manages its _highlight state internally. This example has two State objects, _ParentWidgetState and _TapboxCState.

_ParentWidgetState 对象:

The _ParentWidgetState object:

_TapboxCState 对象:

The _TapboxCState object:

import 'package:flutter/material.dart';

//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({Key? key}) : super(key: key);

  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  const TapboxC({
    Key? key,
    this.active = false,
    required this.onChanged,
  }) : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  @override
  _TapboxCState createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  @override
  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: const TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700]!,
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

另一种实现可能会将高亮状态导出到父 widget,同时保持 active 状态为内部,但如果你要求某人使用该 TapBox,他们可能会抱怨说没有多大意义。开发人员只会关心该框是否处于活动状态。开发人员可能不在乎高亮显示是如何管理的,并且倾向于让 TapBox 处理这些细节。

An alternate implementation might have exported the highlight state to the parent while keeping the active state internal, but if you asked someone to use that tap box, they’d probably complain that it doesn’t make much sense. The developer cares whether the box is active. The developer probably doesn’t care how the highlighting is managed, and prefers that the tap box handles those details.


其他交互式 widgets

Other interactive widgets

Flutter 提供各种按钮和类似的交互式 widget。这些 widget 中的大多数都实现了 Material Design guidelines,它们定义了一组具有质感的 UI 组件。

Flutter offers a variety of buttons and similar interactive widgets. Most of these widgets implement the Material Design guidelines, which define a set of components with an opinionated UI.

如果你愿意,你可以使用 GestureDetector 来给任何自定义 widget 添加交互性。你可以在 管理状态 中找到 GestureDetector 的示例。同时你也可以在 Flutter cookbook处理点击 中学习更多关于 GestureDetector 的内容。

If you prefer, you can use GestureDetector to build interactivity into any custom widget. You can find examples of GestureDetector in Managing state. Learn more about the GestureDetector in Handle taps, a recipe in the Flutter cookbook.

当你需要交互性时,最容易的是使用预制的 widget。这是预置 widget 部分列表:

When you need interactivity, it’s easiest to use one of the prefabricated widgets. Here’s a partial list:

标准 widgets

Standard widgets

Material 组件

Material Components

资源

Resources

以下资源可能会在给您的应用添加交互的时候有所帮助。

The following resources might help when adding interactivity to your app.

手势Flutter 实用教程 里的一个小节。

Gestures, a section in the Flutter cookbook.

处理手势widgets 介绍 文档中一部分
如何创建一个按钮并使其响应用户动作。

Handling gestures, a section in Introduction to widgets
How to create a button and make it respond to input.

点击、拖动和其他手势
Flutter 手势机制的描述。

Gestures in Flutter
A description of Flutter’s gesture mechanism.

Flutter API 文档
所有 Flutter API 的参考文档。

Flutter API documentation
Reference documentation for all of the Flutter libraries.

Flutter Gallery 应用,代码仓库
一个 Demo 应用程序,展示了许多 Material 和其他 Flutter 功能。

Flutter Gallery running app, repo
Demo app showcasing many Material components and other Flutter features.

Flutter 的分层设计 (视频)
此视频包含有关有状态和无状态 widget 的信息。由 Google 工程师 Ian Hickson 讲解。

Flutter’s Layered Design (video)
This video includes information about state and stateless widgets. Presented by Google engineer, Ian Hickson.

添加资源和图片

目录

Flutter 应用程序包含代码和 assets(也为资源)。资源是被打包到应用程序安装包中,可以在运行时访问的一种文件。常见的资源类型包括静态数据(例如 JSON 文件),配置文件,图标和图片(JPEG,WebP,GIF,动画 WebP / GIF,PNG,BMP 和 WBMP)。

Flutter apps can include both code and assets (sometimes called resources). An asset is a file that is bundled and deployed with your app, and is accessible at runtime. Common types of assets include static data (for example, JSON files), configuration files, icons, and images (JPEG, WebP, GIF, animated WebP/GIF, PNG, BMP, and WBMP).

指定资源

Specifying assets

Flutter 使用 pubspec.yaml 文件,位于项目根目录,来识别应用程序所需的资源。

Flutter uses the pubspec.yaml file, located at the root of your project, to identify assets required by an app.

下面举个例子:

Here is an example:

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

如果要包含一个目录下的所有 assets,需要在目录名称的结尾加上 /

To include all assets under a directory, specify the directory name with the / character at the end:

flutter:
  assets:
    - directory/
    - directory/subdirectory/

资源打包

Asset bundling

assets 部分的 flutter 部分需要指定包含在应用程序中的文件。每个资源都通过相对于 pubspec.yaml 文件所在位置的路径进行标识。资源的声明顺序是无关紧要的。资源的实际目录可以是任意文件夹(在第一个样例中是 assets,其他的是 directory

The assets subsection of the flutter section specifies files that should be included with the app. Each asset is identified by an explicit path (relative to the pubspec.yaml file) where the asset file is located. The order in which the assets are declared doesn’t matter. The actual directory name used (assets in first example or directory in the above example) doesn’t matter.

在一次构建中,Flutter 将 assets 放到 asset bundle 的特殊归档中,以便应用程序在运行时读取它们。

During a build, Flutter places assets into a special archive called the asset bundle that apps read from at runtime.

资源变体

Asset variants

构建过程支持 asset 变体:不同版本的资源可能会显示在不同的上下文中。在 pubspec.yamlassets 部分中指定的资源路径,会在构建过程中,查找同级子目录中相同名称的任何文件。这些文件会与指定的资源一起被打包在资源 bundle 中。

The build process supports the notion of asset variants: different versions of an asset that might be displayed in different contexts. When an asset’s path is specified in the assets section of pubspec.yaml, the build process looks for any files with the same name in adjacent subdirectories. Such files are then included in the asset bundle along with the specified asset.

例如,你的应用程序目录中有以下文件:

For example, if you have the following files in your application directory:

.../pubspec.yaml
.../graphics/my_icon.png
.../graphics/background.png
.../graphics/dark/background.png
...etc.

…同时 pubspec.yaml 文件包含:

And your pubspec.yaml file contains the following:

flutter:
  assets:
    - graphics/background.png

那么这两个图片: graphics/background.pnggraphics/dark/background.png 将被打包在你的资源 bundle 中。前者被称为是 main asset,后者被称为是一种变体(variant)。

Then both graphics/background.png and graphics/dark/background.png are included in your asset bundle. The former is considered the main asset, while the latter is considered a variant.

如果指定的是 graphics 目录:

If, on the other hand, the graphics directory is specified:

flutter:
  assets:
    - graphics/

… 那么 graphics/my_icon.pnggraphics/background.pnggraphics/dark/background.png 同时被包含。

Then the graphics/my_icon.png, graphics/background.png and graphics/dark/background.png files are also included.

在选择当前设备分辨率的图片时,Flutter 会使用资源变体;见下文。将来,这种机制可能会扩展到本地化、阅读提示等方面。

Flutter uses asset variants when choosing resolution-appropriate images. In the future, this mechanism might be extended to include variants for different locales or regions, reading directions, and so on.

加载 assets

Loading assets

你的应用程序可以通过 AssetBundle 对象访问其资源。

Your app can access its assets through an AssetBundle object.

Asset bundle 通过指定一个逻辑键(key),允许你读取 string/text(loadString)和 image/binary(load)。在编译期间,这个逻辑键(key)会映射在 pubspec.yaml 中指定的资源路径。

The two main methods on an asset bundle allow you to load a string/text asset (loadString()) or an image/binary asset (load()) out of the bundle, given a logical key. The logical key maps to the path to the asset specified in the pubspec.yaml file at build time.

加载文本 assets

Loading text assets

每个 Flutter 应用程序都有一个 rootBundle 对象,可以轻松访问主资源 bundle 。还可以直接使用 package:flutter/services.dart 中全局静态的 rootBundle 来加载资源。

Each Flutter app has a rootBundle object for easy access to the main asset bundle. It is possible to load assets directly using the rootBundle global static from package:flutter/services.dart.

但是,如果获取当前 BuildContextAssetBundle,建议使用 DefaultAssetBundle。这种方式不是使用应用程序构建的默认资源 bundle,而是让父级 widget 在运行时替换的不同的 AssetBundle,这对于本地化或测试场景很有用。

However, it’s recommended to obtain the AssetBundle for the current BuildContext using DefaultAssetBundle, rather than the default asset bundle that was built with the app; this approach enables a parent widget to substitute a different AssetBundle at run time, which can be useful for localization or testing scenarios.

通常,你可以从应用程序运行时的 rootBundle 中,间接使用 DefaultAssetBundle.of() 来加载资源(例如 JSON 文件)。

Typically, you’ll use DefaultAssetBundle.of() to indirectly load an asset, for example a JSON file, from the app’s runtime rootBundle.

在 Widget 上下文之外,或 AssetBundle 的句柄不可用时,你可以使用 rootBundle 直接加载这些 assets,例如:

Outside of a Widget context, or when a handle to an AssetBundle is not available, you can use rootBundle to directly load such assets. For example:

import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}

加载图片

Loading images

Flutter 可以为当前设备加载适合其分辨率的图像。

Flutter can load resolution-appropriate images for the current device pixel ratio.

声明分辨率相关的图片 assets

Declaring resolution-aware image assets

AssetImage 可以将逻辑请求资源映射到最接近当前 设备像素比 的资源。为了使这种映射起作用,应该根据特定的目录结构来保存资源:

AssetImage understands how to map a logical requested asset onto one that most closely matches the current device pixel ratio. In order for this mapping to work, assets should be arranged according to a particular directory structure:

.../image.png
.../Mx/image.png
.../Nx/image.png
...etc.

…其中 MN 是数字标识符,对应于其中包含的图像的分辨率,换句话说,它们指定不同设备像素比例的图片。

Where M and N are numeric identifiers that correspond to the nominal resolution of the images contained within. In other words, they specify the device pixel ratio that the images are intended for.

主资源默认对应于 1.0 倍的分辨率图片。比如下面的图片 my_icon.png

The main asset is assumed to correspond to a resolution of 1.0. For example, consider the following asset layout for an image named my_icon.png:

.../my_icon.png
.../2.0x/my_icon.png
.../3.0x/my_icon.png

而在设备像素比率为 1.8 的设备上,对应是 .../2.0x/my_icon.png 。如果是 2.7 的设备像素比,对应是 .../3.0x/my_icon.png

On devices with a device pixel ratio of 1.8, the asset .../2.0x/my_icon.png is chosen. For a device pixel ratio of 2.7, the asset .../3.0x/my_icon.png is chosen.

如果在 Image widget 上未指定渲染图像的宽度和高度,通常会扩展资源来保证与主资源相同的屏幕空间量,并不是相同的物理像素,只是分辨率更高。换句话说,.../my_icon.png 是 72 px 乘 72 px,那么 .../3.0x/my_icon.png 应该是 216 px 乘 216 px;但如果未指定宽度和高度,它们都将渲染为 72 px 乘 72 px(以逻辑像素为单位)。

If the width and height of the rendered image are not specified on the Image widget, the nominal resolution is used to scale the asset so that it occupies the same amount of screen space as the main asset would have, just with a higher resolution. That is, if .../my_icon.png is 72px by 72px, then .../3.0x/my_icon.png should be 216px by 216px; but they both render into 72px by 72px (in logical pixels), if width and height are not specified.

pubspec.yaml 中资源部分的每一项都应与实际文件相对应,除过主资源节点。当主资源缺少某个文件时,会按分辨率从低到高的顺序去选择,也就是说 1x 中没有的话会在 2x 中找,2x 中还没有的话就在 3x 中找。该条目需要在 pubspec.yaml 中指定。

Each entry in the asset section of the pubspec.yaml should correspond to a real file, with the exception of the main asset entry. If the main asset entry doesn’t correspond to a real file, then the asset with the lowest resolution is used as the fallback for devices with device pixel ratios below that resolution. The entry should still be included in the pubspec.yaml manifest, however.

加载 images

Loading images

加载图片,请在 widget 的 build 方法中使用 AssetImage 类。

To load an image, use the AssetImage class in a widget’s build() method.

例如,你的应用程序可以从上面的资源声明中加载背景图片:

For example, your app can load the background image from the asset declarations above:

return const Image(image: AssetImage('graphics/background.png'));

使用默认的资源 bundle 加载资源时,系统会自动处理分辨率等。(如果你使用一些更低级别的类,如 ImageStreamImageCache,你需要注意 scale 相关的参数)。

Anything using the default asset bundle inherits resolution awareness when loading images. (If you work with some of the lower level classes, like ImageStream or ImageCache, you’ll also notice parameters related to scale.)

依赖包中的资源图片

Asset images in package dependencies

加载依赖 package 中的图像,必须给 AssetImage 提供 package 参数。

To load an image from a package dependency, the package argument must be provided to AssetImage.

例如,你的应用程序依赖于一个名为 my_icons 的 package,它的目录结构如下:

For instance, suppose your application depends on a package called my_icons, which has the following directory structure:

.../pubspec.yaml
.../icons/heart.png
.../icons/1.5x/heart.png
.../icons/2.0x/heart.png
...etc.

然后加载 image, 使用:

To load the image, use:

return const AssetImage('icons/heart.png', package: 'my_icons');

package 使用本身的 Assets 也需要加上 package 参数来获取。

Assets used by the package itself should also be fetched using the package argument as above.

打包 assets

Bundling of package assets

如果期望的资源文件被指定在 package 的 pubspec.yaml 文件中,它会被自动打包到应用程序中。特别是,package 本身使用的资源必须在 pubspec.yaml 中指定。

If the desired asset is specified in the pubspec.yaml file of the package, it’s bundled automatically with the application. In particular, assets used by the package itself must be specified in its pubspec.yaml.

package 也可以选择在其 lib/ 文件夹中包含未在 pubspec.yaml 文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在 pubspec.yaml 中指定包含哪些图像。例如,一个名为 fancy_backgrounds 的包,可能包含以下文件:

A package can also choose to have assets in its lib/ folder that are not specified in its pubspec.yaml file. In this case, for those images to be bundled, the application has to specify which ones to include in its pubspec.yaml. For instance, a package named fancy_backgrounds could have the following files:

.../lib/backgrounds/background1.png
.../lib/backgrounds/background2.png
.../lib/backgrounds/background3.png

总而言之,要包含第一张图像,必须在 pubspec.yamlassets 部分中声明它:

To include, say, the first image, the pubspec.yaml of the application should specify it in the assets section:

flutter:
  assets:
    - packages/fancy_backgrounds/backgrounds/background1.png

lib/ 是隐含的,所以它不应该包含在资源路径中。

The lib/ is implied, so it should not be included in the asset path.

平台共享 assets

Sharing assets with the underlying platform

在不同平台读取 Flutter assets, Android 是通过 AssetManager,iOS 是 NSBundle

Flutter assets are readily available to platform code using the AssetManager on Android and NSBundle on iOS.

在 Android 中加载 Flutter 资源文件

Loading Flutter assets in Android

在 Android 平台上,assets 通过 AssetManager API 读取。通过 PluginRegistry.RegistrarlookupKeyForAsset 方法,或者 FlutterViewgetLookupKeyForAsset 方法来获取文件路径,然后 AssetManageropenFd 根据文件路径得到文件描述符。开发插件时可以使用 PluginRegistry.Registrar,而开发应用程序使用平台视图时,FlutterView 是最好的选择。

On Android the assets are available through the AssetManager API. The lookup key used in, for instance openFd, is obtained from lookupKeyForAsset on PluginRegistry.Registrar or getLookupKeyForAsset on FlutterView. PluginRegistry.Registrar is available when developing a plugin while FlutterView would be the choice when developing an app including a platform view.

举个例子,假设你在 pubspec.yaml 中这样指定:

As an example, suppose you have specified the following in your pubspec.yaml

flutter:
  assets:
    - icons/heart.png

在你的 Flutter 应用程序对应以下结构。

This reflects the following structure in your Flutter app.

.../pubspec.yaml
.../icons/heart.png
...etc.

想要在 Java 插件中访问 icons/heart.png

To access icons/heart.png from your Java plugin code, do the following:

AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/heart.png");
AssetFileDescriptor fd = assetManager.openFd(key);

在 iOS 中加载 Flutter 资源文件

Loading Flutter assets in iOS

在 iOS 平台上,assets 资源文件通过 mainBundle 读取。通过 pathForResource:ofType:lookupKeyForAsset 或者 lookupKeyForAsset:fromPackage: 方法获取文件路径,同样,FlutterViewControllerlookupKeyForAsset: 或者 lookupKeyForAsset:fromPackage: 方法也可以获取文件路径。开发插件时可以使用 FlutterPluginRegistrar,而开发应用程序使用平台视图时, FlutterViewController 是最好的选择。

On iOS the assets are available through the mainBundle. The lookup key used in, for instance pathForResource:ofType:, is obtained from lookupKeyForAsset or lookupKeyForAsset:fromPackage: on FlutterPluginRegistrar, or lookupKeyForAsset: or lookupKeyForAsset:fromPackage: on FlutterViewController. FlutterPluginRegistrar is available when developing a plugin while FlutterViewController would be the choice when developing an app including a platform view.

举个例子,假设你的 Flutter 配置和上面一样。

As an example, suppose you have the Flutter setting from above.

要在 Objective-C 插件中访问 icons/heart.png

To access icons/heart.png from your Objective-C plugin code you would do the following:

NSString* key = [registrar lookupKeyForAsset:@"icons/heart.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];

这有一个更完整的实例可以理解 Flutter 的应用:video_player plugin

For a more complete example, see the implementation of the Flutter video_player plugin on pub.dev.

pub.dev 上的 ios_platform_images plugin 将这些逻辑封装成方便的类别。它允许编写:

The ios_platform_images plugin on pub.dev wraps up this logic in a convenient category. You fetch an image as follows:

Objective-C:

[UIImage flutterImageWithName:@"icons/heart.png"];

Swift:

UIImage.flutterImageNamed("icons/heart.png")

在 Flutter 中加载 iOS 的图片

Loading iOS images in Flutter

When implementing Flutter by adding it to an existing iOS app, you might have images hosted in iOS that you want to use in Flutter. To accomplish that, use the ios_platform_images plugin available on pub.dev.

平台 assets

Platform assets

某些场景可以直接在平台项目中使用 assets。以下是在 Flutter 框架加载并运行之前使用资源的两种常见情况。

There are other occasions to work with assets in the platform projects directly. Below are two common cases where assets are used before the Flutter framework is loaded and running.

更新桌面图标

Updating the app icon

更新你的 Flutter 应用程序启动图标,和原生 Android 或 iOS 应用程序中更新启动图标的方法相同。

Updating a Flutter application’s launch icon works the same way as updating launch icons in native Android or iOS applications.

Launch icon

Android

在 Flutter 项目的根目录中,导航到 .../android/app/src/main/res 路径。各种位图资源文件夹,比如 mipmap-hdpi,已包含占位符图像 ic_launcher.png。只需按照 Android 开发者指南 中的说明,将其替换为所需的资源,并遵守每种屏幕分辨率的建议图标大小标准。

In your Flutter project’s root directory, navigate to .../android/app/src/main/res. The various bitmap resource folders such as mipmap-hdpi already contain placeholder images named ic_launcher.png. Replace them with your desired assets respecting the recommended icon size per screen density as indicated by the Android Developer Guide.

Android icon location

iOS

在你的 Flutter 项目的根目录中,导航到 .../ios/Runner 路径。该目录中 Assets.xcassets/AppIcon.appiconset已经包含占位符图片,只需将它们替换为适当大小的图片,并且根据 iOS 开发指南,文件名称保持不变。

In your Flutter project’s root directory, navigate to .../ios/Runner. The Assets.xcassets/AppIcon.appiconset directory already contains placeholder images. Replace them with the appropriately sized images as indicated by their filename as dictated by the Apple Human Interface Guidelines. Keep the original file names.

iOS icon location

更新启动图

Updating the launch screen

Launch screen

在 Flutter 框架加载时,Flutter 会使用原生平台机制绘制启动页。此启动页将持续到 Flutter 渲染应用程序的第一帧。

Flutter also uses native platform mechanisms to draw transitional launch screens to your Flutter app while the Flutter framework loads. This launch screen persists until Flutter renders the first frame of your application.

Android

将启动屏幕「splash screen」添加到你的 Flutter 应用程序,请导航至 .../android/app/src/main 路径。在 res/drawable/launch_background.xml 文件中,通过使用 图层列表 XML 来实现自定义启动页。现有模板提供了一个示例,用于将图片添加到白色启动页的中间(注释代码中)。你也可以取消注释使用 可绘制对象资源 来实现预期效果。

To add a “splash screen” to your Flutter application, navigate to .../android/app/src/main. In res/drawable/launch_background.xml, use this layer list drawable XML to customize the look of your launch screen. The existing template provides an example of adding an image to the middle of a white splash screen in commented code. You can uncomment it or use other drawables to achieve the intended effect.

更多详细信息,请查看 在 Android 应用中添加闪屏页与启动页

For more details, see Adding a splash screen to your mobile app.

iOS

将图片添加到启动屏幕「splash screen」的中心,请导航至 .../ios/Runner 路径。在 Assets.xcassets/LaunchImage.imageset ,拖入图片,并命名为 LaunchImage.pngLaunchImage@2x.pngLaunchImage@3x.png。如果你使用不同的文件名,那你还必须更新同一目录中的 Contents.json 文件中对应的名称。

To add an image to the center of your “splash screen”, navigate to .../ios/Runner. In Assets.xcassets/LaunchImage.imageset, drop in images named LaunchImage.png, LaunchImage@2x.png, LaunchImage@3x.png. If you use different filenames, update the Contents.json file in the same directory.

你也可以通过打开 .../ios/Runner.xcworkspace ,完全自定义 storyboard。在 Project Navigator 中导航到 Runner/Runner ,然后打开 Assets.xcassets 拖入图片,或者在 LaunchScreen.storyboard 中使用 Interface Builder 进行自定义。

You can also fully customize your launch screen storyboard in Xcode by opening .../ios/Runner.xcworkspace. Navigate to Runner/Runner in the Project Navigator and drop in images by opening Assets.xcassets or do any customization using the Interface Builder in LaunchScreen.storyboard.

Adding launch icons in Xcode

更多详细信息,请查看 在 Android 应用中添加闪屏页与启动页

For more details, see Adding a splash screen to your mobile app.

路由和导航

Flutter 有一个命令式路由机制,即 Navigator 组件,还有一个更为惯用的声明式路由机制(类似于 widget 中使用的 build 方法),即 Router 组件。

Flutter has an imperative routing mechanism, the Navigator widget, and a more idiomatic declarative routing mechanism (which is similar to build methods as used with widgets), the Router widget.

这两个系统可以一起使用(事实上,声明式系统是使用命令式系统构建的)。

The two systems can be used together (indeed, the declarative system is built using the imperative system).

对于小型应用程序来说,通常只需通过 MaterialApp 构造函数中的 MaterialApp.routes 属性,加以使用 Navigator API,就可以很好地提供服务。

Typically, small applications are served well by just using the Navigator API, via the MaterialApp constructor’s MaterialApp.routes property.

要了解 Navigator 及其命令式 API,参阅 Flutter 教程 中的 Navigator 教程Navigator API 文档。

To learn about Navigator and its imperative API, see the Navigation recipes in the Flutter cookbook, and the Navigator API docs.

通过 MaterialApp.router 构造函数,Router API 可以更好地为复杂应用程序服务。它需要更多的前期工作来描述如何解析应用程序的复杂路由,以及如何将应用程序的状态映射到页面集合,但从长远的角度来看会使路由的控制更具表现力。

More elaborate applications are usually better served by the Router API, via the MaterialApp.router constructor. This requires some more up-front work to describe how to parse deep links for your application and how to map the application state to the set of active pages, but is more expressive on the long run.

要了解 Router 和声明式方法,参阅 学习 Flutter 的新导航和路由系统Router API 文档。

To learn about Router and the declarative approach, see Learning Flutter’s new navigation and routing system, and the Router API docs.

Deep linking

目录

Flutter supports deep linking on iOS, Android, and web browsers. Opening a URL displays that screen in your app. With the following steps, you can launch and display routes by using named routes (either with the routes parameter or onGenerateRoute), or by using the Router widget.

If you’re running the app in a web browser, there’s no additional setup required. Route paths are handled in the same way as an iOS or Android deep link. By default, web apps read the deep link path from the url fragment using the pattern: /#/path/to/app/screen, but this can be changed by configuring the URL strategy for your app.

To follow along, clone the Navigation and Routing in flutter/samples.

Enable deep linking on Android

Add a metadata tag and intent filter to AndroidManifest.xml inside the <activity> tag with the ".MainActivity" name:

<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="http" android:host="flutterbooksample.com" />
    <data android:scheme="https" />
</intent-filter>

A full restart is required to apply these changes.

Test on Android emulator

To test with an Android emulator, give the adb command an intent where the host name matches the name defined in AndroidManifest.xml:

adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "http://flutterbooksample.com/book/1"

For more details, see the Verify Android App Links documentation in the Android docs.

Enable deep linking on iOS

Add two new keys to Info.plist in the ios/Runner directory:

<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
    <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>flutterbooksample.com</string>
    <key>CFBundleURLSchemes</key>
    <array>
    <string>customscheme</string>
    </array>
    </dict>
</array>

The CFBundleURLName is a unique URL used to distinguish your app from others that use the same scheme. The scheme (customscheme://) can also be unique.

A full restart is required to apply these changes.

Test on iOS simulator

Use the xcrun command to test on the iOS Simulator:

xcrun simctl openurl booted customscheme://flutterbooksample.com/book/1 

Migrating from plugin-based deep linking

If you have written a plugin to handle deep links, as described in “Deep Links and Flutter applications” on Medium, it will continue to work until you opt-in to this behavior by adding FlutterDeepLinkingEnabled to Info.plist or flutter_deeplinking_enabled to AndroidManifest.xml, respectively.

Behavior

The behavior varies slightly based on the platform and whether the app is launched and running.

Platform / Scenario Using Navigator Using Router
iOS (not launched) App gets initialRoute (“/”) and a short time after gets a pushRoute App gets initialRoute (“/”) and a short time after uses the RouteInformationParser to parse the route and call RouterDelegate.setNewRoutePath, which configures the Navigator with the corresponding Page.
Android - (not launched) App gets initialRoute containing the route (“/deeplink”) App gets initialRoute (“/deeplink”) and passes it to the RouteInformationParser to parse the route and call RouterDelegate.setNewRoutePath, which configures the Navigator with the corresponding Pages.
iOS (launched) pushRoute is called Path is parsed, and the Navigator is configured with a new set of Pages.
Android (launched) pushRoute is called Path is parsed, and the Navigator is configured with a new set of Pages.

After upgrading to the Router widget, your app has the ability to replace the current set of pages when a new deep link is opened while the app is running.

See also

Learning Flutter’s new navigation and routing system provides an introduction to the Router system.

配置 Web 应用的 URL 策略

目录

Flutter Web 应用支持两种基于 URL 的路由的配置方式:

Flutter web apps support two ways of configuring URL-based navigation on the web:

Hash(默认)
路径使用 # + 锚点标识符 读写,例如:flutterexample.dev/#/path/to/screen

Hash (default)
Paths are read and written to the hash fragment. For example, flutterexample.dev/#/path/to/screen.

Path
路径使用非 # 读写,例如:flutterexample.dev/path/to/screen

Path
Paths are read and written without a hash. For example, flutterexample.dev/path/to/screen.

使用 setUrlStrategy API 设置 HashUrlStrategy 或者 PathUrlStrategy

These are set using the setUrlStrategy API with either a HashUrlStrategy or PathUrlStrategy.

配置 URL 策略

Configuring the URL strategy

setUrlStrategy API 只能在 Web 平台使用。下方的内容将介绍如何在 Web 平台下(仅在 Web 平台可用)使用条件引入的方式来调用此方法。

The setUrlStrategy API can only be called on the web. The following instructions show how to use a conditional import to call this function on the web, but not on other platforms.

  1. Include the flutter_web_plugins package and call the setUrlStrategy function before your app runs:引入 flutter_web_plugins package,并在你的应用启动前调用 setUrlStrategy 方法:

      dependencies:
        flutter_web_plugins:
          sdk: flutter
    
  2. Create a lib/configure_nonweb.dart file with the following code:创建如下的 lib/configure_nonweb.dart 文件:

      void configureApp() {
        // No-op.
      }
    
  3. Create a lib/configure_web.dart file with the following code:创建如下的 lib/configure_web.dart 文件:

      import 'package:flutter_web_plugins/flutter_web_plugins.dart';
    
      void configureApp() {
        setUrlStrategy(PathUrlStrategy());
      }
    
  4. Open `lib/main.dart` and conditionally import `configure_web.dart` when the `html` package is available, or `configure_nonweb.dart` when it isn't:
    打开 `lib/main.dart`,当 `html` 包可用时,使用条件引入的方式引入 `configure_web.dart`,否则引入 `configure_nonweb.dart`:

      import 'package:flutter/material.dart';
      import 'configure_nonweb.dart' if (dart.library.html) 'configure_web.dart';
    
      void main() {
        configureApp();
      runApp(MyApp());
      }
    

将 Flutter 应用部署在子目录下

Hosting a Flutter app at a non-root location

更新 web/index.html 中的 <base href="/"> 标签为你的应用部署路径。例如:如果你期望将 Flutter 应用部署在 myapp.dev/flutter_app,则更改此标签为 <base href="/flutter_app">

Update the <base href="/"> tag in web/index.html to the path where your app is hosted. For example, to host your Flutter app at myapp.dev/flutter_app, change this tag to <base href="/flutter_app/">.

动画效果介绍

目录

Well-designed animations make a UI feel more intuitive, contribute to the slick look and feel of a polished app, and improve the user experience. Flutter’s animation support makes it easy to implement a variety of animation types. Many widgets, especially Material widgets, come with the standard motion effects defined in their design spec, but it’s also possible to customize these effects.

选择一种实现方式

Choosing an approach

在 Flutter 中创建动画可以有多种不同实现方式。那么,究竟哪种才是最适合你的呢?为了帮助你更好的理解它,你可以观看下面的视频, 如何在 Flutter 中选择合适的动画 Widget (同时也发布了一篇 配套文章。)

There are different approaches you can take when creating animations in Flutter. Which approach is right for you? To help you decide, check out the video, How to choose which Flutter Animation Widget is right for you? (Also published as a companion article.)

(若想要深入了解它的决策流程,请观看在 Flutter Europe 社区账号发布的 在 Flutter 中使用动画的正确选择)视频。

(To dive deeper into the decision process, watch the Animations in Flutter done right video, presented at Flutter Europe.)

正如视频中说的那样,下面的决策树将帮助你挑选实现 Flutter 动画的正确方式:

As shown in the video, the following decision tree helps you decide what approach to use when implementing a Flutter animation:

The animation decision tree

如果内置的隐式动画(最简单的动画)已经能够满足你的需求,请观看 隐式动画基础。(同时也发布了一篇 配套文章。)

If a pre-packaged implicit animation (the easiest animation to implement) suits your needs, watch Animation basics with implicit animations. (Also published as a companion article.)

要创建一个自定义的隐式动画,请观看 使用 TweenAnimationBuilder 创建独特的隐式动画。(同时也发布了一篇 配套文章。)

To create a custom implicit animation, watch Creating your own custom implicit animations with TweenAnimationBuilder. (Also published as a companion article.)

要创建显式动画(手动控制,而不是让框架控制),你可以使用内置的其中一个显式动画类来实现。更多有关信息,请观看 使用内置显式动画。(同时也发布了一篇 配套文章。)

To create an explicit animation (where you control the animation, rather than letting the framework control it), perhaps you can use one of the built-in explicit animations classes. For more information, watch Making your first directional animations with built-in explicit animations. (Also published as a companion article.)

如果你需要从头开始构建显式动画,请观看 通过 AnimatedBuilder 和 AnimatedWidget 创建一个自定义动画。(同时也发布了一篇配套文章。)

If you need to build an explicit animation from scratch, watch Creating custom explicit animations with AnimatedBuilder and AnimatedWidget. (Also published as a companion article.)

想要更深入的理解动画在 Flutter 中的工作方式,请观看 深入理解动画。(同时也发布了一篇 配套文章。)

For a deeper understanding of just how animations work in Flutter, watch Animation deep dive. (Also published as a companion article.)

Codelabs, 教程和文章

Codelabs, tutorials, and articles

通过下面的资源可以很好的学习 Flutter 动画框架。这些文档循序渐进地讲解如何编写动画代码。

The following resources are a good place to start learning the Flutter animation framework. Each of these documents shows how to write animation code.

Animation types

动画分为两类:补间动画和基于物理动画。下面将解释这些术语的含义,并帮助您找到更多相关资源。在一些情况下,我们现有的最佳文档是 Flutter gallery 中的示例代码。

Generally, animations are either tween- or physics-based. The following sections explain what these terms mean, and point you to resources where you can learn more.

补间动画

Tween animation

补间动画是“介于两者之间”的缩写。在补间动画中,定义了起点和终点以及时间轴,再定义过渡时间和速度的曲线。然后框架会计算如何从起点过渡到终点。

Short for in-betweening. In a tween animation, the beginning and ending points are defined, as well as a timeline, and a curve that defines the timing and speed of the transition. The framework calculates how to transition from the beginning point to the end point.

上文列出的文档,比如 在 Flutter 应用里实现动画效果 并不是特别针对补间动画的,但是其示例中使用了补间动画。

The documents listed above, such as the animations tutorial are not about tweening, specifically, but they use tweens in their examples.

基于物理基础的动画

Physics-based animation

在基于物理基础的动画中,动作是模拟真实世界的行为来进行建模的。举个例子,当您抛球时,球落地的时间和位置取决于抛出的速度和距离地面的高度。类似地,附在弹簧上的球和附在绳子上的球掉落(和反弹)方式是不一样的。

In physics-based animation, motion is modeled to resemble real-world behavior. When you toss a ball, for example, where and when it lands depends on how fast it was tossed and how far it was from the ground. Similarly, dropping a ball attached to a spring falls (and bounces) differently than dropping a ball attached to a string.

预置动画

Pre-canned animations

如果你在使用 Material widgets,你也许想要看看 pub.dev 上的 animations package。这个 package 包含了以下内置常用模式: Container 变换、共享轴变化、渐变穿透和渐变变换。

If you are using Material widgets, you might check out the animations package available on pub.dev. This package contains pre-built animations for the following commonly used patterns: Container transforms, shared axis transitions, fade through transitions, and fade transitions.

常见动画模式

Common animation patterns

大多数 UX 或动效设计师在设计 UI 时都会寻找主要动画模式。本章的列表将介绍一些常见的动画模式,并向你介绍更多学习它们的地方。

Most UX or motion designers find that certain animation patterns are used repeatedly when designing a UI. This section lists some of the commonly used animation patterns, and tells you where to learn more.

列表或网格动画

Animated list or grid

这种模式用于在列表或网格中添加或删除元素。

This pattern involves animating the addition or removal of elements from a list or grid.

共享元素转换

Shared element transition

在这个模式中,用户从页面中选择一个元素,通常是图像,然后 UI 会在新页面中为指定元素添加动画,并生成更多细节。在 Flutter 中,您可以通过 Hero widget 轻松实现路径(页面)间的共享元素转换动画。

In this pattern, the user selects an element—often an image—from the page, and the UI animates the selected element to a new page with more detail. In Flutter, you can easily implement shared element transitions between routes (pages) using the Hero widget.

交织动画

Staggered animation

动画被分解成较小的动作,其中一些动作被延迟。这些小动画可以是连续的,也可以部分或完全重叠。

Animations that are broken into smaller motions, where some of the motion is delayed. The smaller animations might be sequential, or might partially or completely overlap.

其他资源

Other resources

以下链接可以了解更多 Flutter 动画:

Learn more about Flutter animations at the following links:

动画概览

目录

Flutter 中的动画系统基于 Animation。 Widgets 可以直接将这些动画合并到自己的 build 方法中来读取它们的当前值或者监听它们的状态变化,或者可以将其作为的更复杂动画的基础传递给其他 widgets。

The animation system in Flutter is based on typed Animation objects. Widgets can either incorporate these animations in their build functions directly by reading their current value and listening to their state changes or they can use the animations as the basis of more elaborate animations that they pass along to other widgets.

Animation

动画系统的首要组成部分就是 Animation 类。一个动画表现为可在它的生命周期内发生变化的特定类型的值。大多数需要执行动画的 widgets 都需要接收一个 Animation 对象作为参数,从而能从中获取到动画的当前状态值以及应该监听哪些具体值的更改。

The primary building block of the animation system is the Animation class. An animation represents a value of a specific type that can change over the lifetime of the animation. Most widgets that perform an animation receive an Animation object as a parameter, from which they read the current value of the animation and to which they listen for changes to that value.

addListener

每当动画的状态值发生变化时,动画都会通知所有通过 addListener 添加的监听器。通常,一个正在监听动画的 State 对象会调用自身的 setState 方法,将自身传入这些监听器的回调函数来通知 widget 系统需要根据新状态值进行重新构建。

Whenever the animation’s value changes, the animation notifies all the listeners added with addListener. Typically, a State object that listens to an animation calls setState on itself in its listener callback to notify the widget system that it needs to rebuild with the new value of the animation.

这种模式非常常见,所以有两个 widgets 可以帮助其他 widgets 在动画改变值时进行重新构建: AnimatedWidgetAnimatedBuilder。第一个是 AnimatedWidget,对于无状态动画 widgets 来说是尤其有用的。要使用 AnimatedWidget,只需继承它并实现一个 build 方法。第二个是 AnimatedBuilder,对于希望将动画作为复杂 widgets 的 build 方法的其中一部分的情况非常有用。要使用 AnimatedBuilder,只需构造 widget 并将 AnimatedBuilder 传递给 widget 的 builder 方法。

This pattern is so common that there are two widgets that help widgets rebuild when animations change value: AnimatedWidget and AnimatedBuilder. The first, AnimatedWidget, is most useful for stateless animated widgets. To use AnimatedWidget, simply subclass it and implement the build function. The second, AnimatedBuilder, is useful for more complex widgets that wish to include an animation as part of a larger build function. To use AnimatedBuilder, simply construct the widget and pass it a builder function.

addStatusListener

动画还提供了一个 AnimationStatus,表示动画将如何随时间进行变化。每当动画的状态发生变化时,动画都会通知所有通过 addStatusListener 添加的监听器。通常情况下,动画会从 dismissed 状态开始,表示它处于变化区间的开始点。举例来说,从 0.0 到 1.0 的动画在 dismissed 状态时的值应该是 0.0。动画进行的下一状态可能是 forward(比如从 0.0 到 1.0)或者 reverse(比如从 1.0 到 0.0)。最终,如果动画到达其区间的结束点(比如 1.0),则动画会变成 completed 状态。

Animations also provide an AnimationStatus, which indicates how the animation will evolve over time. Whenever the animation’s status changes, the animation notifies all the listeners added with addStatusListener. Typically, animations start out in the dismissed status, which means they’re at the beginning of their range. For example, animations that progress from 0.0 to 1.0 will be dismissed when their value is 0.0. An animation might then run forward (from 0.0 to 1.0) or perhaps in reverse (from 1.0 to 0.0). Eventually, if the animation reaches the end of its range (1.0), the animation reaches the completed status.

Animation­Controller

要创建动画,首先要创建一个 AnimationController。除了作为动画本身,AnimationController 还可以用来控制动画。例如,你可以通过控制器让动画正向播放 forward 或停止动画 stop。你还可以添加物理模拟效果 fling(例如弹簧效果)来驱动动画。

To create an animation, first create an AnimationController. As well as being an animation itself, an AnimationController lets you control the animation. For example, you can tell the controller to play the animation forward or stop the animation. You can also fling animations, which uses a physical simulation, such as a spring, to drive the animation.

一旦创建了一个动画控制器,你可以基于它来构建其他动画。例如,你可以创建一个 ReverseAnimation,效果是复制一个动画但是将其反向运行(比如从 1.0 到 0.0)。同样,你可以创建一个 CurvedAnimation,效果是用 Curve 来调整动画的值。

Once you’ve created an animation controller, you can start building other animations based on it. For example, you can create a ReverseAnimation that mirrors the original animation but runs in the opposite direction (from 1.0 to 0.0). Similarly, you can create a CurvedAnimation whose value is adjusted by a Curve.

补间动画

Tweens

如果想要在 0.0 到 1.0 的区间之外设置动画,可以使用 Tween<T>,它可以在它的 begin 值和 end 值之间进行插值补间。许多类都有特定的 Tween 子类,它们能提供基于特定类型的插值行为。例如, ColorTween 可以在颜色间进行插值, RectTween 可以在矩形之间进行插值。你可以通过创建自己的 Tween 子类并覆盖其 lerp 方法来定义自己的补间动画。

To animate beyond the 0.0 to 1.0 interval, you can use a Tween<T>, which interpolates between its begin and end values. Many types have specific Tween subclasses that provide type-specific interpolation. For example, ColorTween interpolates between colors and RectTween interpolates between rects. You can define your own interpolations by creating your own subclass of Tween and overriding its lerp function.

补间动画本身只定义了如何在两个值之间进行插值。要获取动画当前帧的具体值,还需要一个动画来确定当前状态。有两种方法可以将补间动画与动画组合在一起以获得动画的具体值:

By itself, a tween just defines how to interpolate between two values. To get a concrete value for the current frame of an animation, you also need an animation to determine the current state. There are two ways to combine a tween with an animation to get a concrete value:

  1. 你可以用 evaluate 方法处理动画的当前值从而得到对应的插值。这种方法对于已经监听动画并因此在动画改变值时重新构建的 widgets 是最有效的。

    You can evaluate the tween at the current value of an animation. This approach is most useful for widgets that are already listening to the animation and hence rebuilding whenever the animation changes value.

  2. 你可以用 animate 方法处理一个动画。相对于返回单个值,animate 方法返回一个包含补间动画插值的新的 Animation。这种方法对于当你想要将新创建的动画提供给另一个 widget 时最有效,它可以直接读取包含补间动画的插值以及监听对应插值的更改。

    You can animate the tween based on the animation. Rather than returning a single value, the animate function returns a new Animation that incorporates the tween. This approach is most useful when you want to give the newly created animation to another widget, which can then read the current value that incorporates the tween as well as listen for changes to the value.

架构

Architecture

动画实际上是由许多核心模块共同构建的。

Animations are actually built from a number of core building blocks.

调度器

Scheduler

SchedulerBinding 是一个暴露出 Flutter 调度原语的单例类。

The SchedulerBinding is a singleton class that exposes the Flutter scheduling primitives.

在这一节,关键原语是帧回调。每当一帧需要在屏幕上显示时, Flutter 的引擎会触发一个 “开始帧” 回调,调度程序会将其多路传输给所有使用 scheduleFrameCallback() 注册的监听器。所有这些回调不管在任意状态或任意时刻都可以收到这一帧的绝对时间戳。由于所有回调收到时间戳都相同,因此这些回调触发的任何动画看起来都是完全同步的,即使它们需要几毫秒才能执行。

For this discussion, the key primitive is the frame callbacks. Each time a frame needs to be shown on the screen, Flutter’s engine triggers a “begin frame” callback that the scheduler multiplexes to all the listeners registered using scheduleFrameCallback(). All these callbacks are given the official time stamp of the frame, in the form of a Duration from some arbitrary epoch. Since all the callbacks have the same time, any animations triggered from these callbacks will appear to be exactly synchronised even if they take a few milliseconds to be executed.

运行器

Tickers

Ticker 类挂载在调度器的 scheduleFrameCallback() 的机制上,来达到每次运行都会触发回调的效果。

The Ticker class hooks into the scheduler’s scheduleFrameCallback() mechanism to invoke a callback every tick.

一个 Ticker 可以被启动和停止,启动时,它会返回一个 Future,这个 FutureTicker 停止时会被改为完成状态。

一个 Ticker 可以被启动和停止,启动时,它会返回一个 Future,这个 FutureTicker 停止时会被改为完成状态。

A Ticker can be started and stopped. When started, it returns a Future that will resolve when it is stopped.

每次运行, Ticker 都会为回调函数提供从 Ticker 开始运行到现在的持续时间。

Each tick, the Ticker provides the callback with the duration since the first tick after it was started.

因为运行器总是会提供在自它们开始运行以来的持续时间,所以所有运行器都是同步的。如果你在两帧之间的不同时刻启动三个运行器,它们都会被同步到相同的开始时间,并随后同步运行。

Because tickers always give their elapsed time relative to the first tick after they were started; tickers are all synchronised. If you start three tickers at different times between two ticks, they will all nonetheless be synchronised with the same starting time, and will subsequently tick in lockstep. Like people at a bus-stop, all the tickers wait for a regularly occurring event (the tick) to begin moving (counting time).

模拟器

Simulations

Simulation 抽象类将相对时间值(运行时间)映射为双精度值,并且有完成的概念。

The Simulation abstract class maps a relative time value (an elapsed time) to a double value, and has a notion of completion.

原则上,模拟器是无状态的,但在实践中,一些模拟器(例如 BouncingScrollSimulationClampingScrollSimulation)在查询时会不可逆地被改变状态。

In principle simulations are stateless but in practice some simulations (for example, BouncingScrollSimulation and ClampingScrollSimulation) change state irreversibly when queried.

针对不同的效果,Simulation 类有 各种具体实现

There are various concrete implementations of the Simulation class for different effects.

Animatables

Animatable 抽象类将双精度值映射为特定类型的值。

The Animatable abstract class maps a double to a value of a particular type.

Animatable 类是无状态和不可变的。

Animatable classes are stateless and immutable.

补间动画

Tweens

Tween<T> 抽象类将名义范围为 0.0-1.0 的双精度值映射到某个类型值(例如 Color 或其他双精度值)。它属于 Animatable

The Tween<T> abstract class maps a double value nominally in the range 0.0-1.0 to a typed value (for example, a Color, or another double). It is an Animatable.

它有一个输出类型(T)的概念,这个输出类型有一个 begin 值和一个end 值,以及在给定输入值的起始值和结束值(名义范围为 0.0-1.0 的双精度值)之间插值(lerp)的方法。

It has a notion of an output type (T), a begin value and an end value of that type, and a way to interpolate (lerp) between the begin and end values for a given input value (the double nominally in the range 0.0-1.0).

Tween 类是无状态和不可变的。

Tween classes are stateless and immutable.

组合 animatables

Composing animatables

Animatable<double>(父类)传递给一个 Animatablechain() 方法会创建一个新的 Animatable 子类,这个子类会先应用父类的映射,然后应用子类的映射。

Passing an Animatable<double> (the parent) to an Animatable’s chain() method creates a new Animatable subclass that applies the parent’s mapping then the child’s mapping.

曲线

Curves

Curve 抽象类将名义范围为 0.0-1.0 的双精度值映射到名义范围为 0.0-1.0 的双精度值。

The Curve abstract class maps doubles nominally in the range 0.0-1.0 to doubles nominally in the range 0.0-1.0.

Curve 类是无状态和不可变的。

Curve classes are stateless and immutable.

动画

Animations

Animation 抽象类提供给定类型的值、动画方向的概念和动画状态和一个监听器接口,这个监听器接口用来注册值或状态的改变时被调用的回调。

The Animation abstract class provides a value of a given type, a concept of animation direction and animation status, and a listener interface to register callbacks that get invoked when the value or status change.

有些 Animation 的子类值是永远不变的(kAlwaysCompleteAnimationkAlwaysDismissedAnimationAlwaysStoppedAnimation),在这些子类上注册回调没有任何效果,因为这些回调永远不会被调用。

Some subclasses of Animation have values that never change (kAlwaysCompleteAnimation, kAlwaysDismissedAnimation, AlwaysStoppedAnimation); registering callbacks on these has no effect as the callbacks are never called.

Animation<double> 变量很特殊,因为它可以被用来表示名义范围为 0.0-1.0 的双精度值,也就是 CurveTween 类以及动画的一些其他子类所期望的输入。

The Animation<double> variant is special because it can be used to represent a double nominally in the range 0.0-1.0, which is the input expected by Curve and Tween classes, as well as some further subclasses of Animation.

有些 Animation 的子类是无状态的,只是将监听器转发给其父级;另外有些是有状态的。

Some Animation subclasses are stateless, merely forwarding listeners to their parents. Some are very stateful.

组合动画

Composable animations

大多数 Animation 子类都采用明确的 “父级提供的” Animation<double>。可以说它们是由父级驱动的。

Most Animation subclasses take an explicit “parent” Animation<double>. They are driven by that parent.

CurvedAnimation 子类接收一个 Animation<double> 类(父级)和几个 Curve 类(正向和反向曲线)作为输入,并使用父级的值作为输入提供给曲线来确定它的输出。 CurvedAnimation 是不可变和无状态的。

The CurvedAnimation subclass takes an Animation<double> class (the parent) and a couple of Curve classes (the forward and reverse curves) as input, and uses the value of the parent as input to the curves to determine its output. CurvedAnimation is immutable and stateless.

ReverseAnimation 子类接收一个 Animation<double> 类作为它的父级,但反转动画所有的值。它假定父级使用名义范围为 0.0-1.0 的双精度值,并返回范围为 1.0-0.0 的值。父级动画的状态和方向也会被反转。 ReverseAnimation 是不可变和无状态的。

The ReverseAnimation subclass takes an Animation<double> class as its parent and reverses all the values of the animation. It assumes the parent is using a value nominally in the range 0.0-1.0 and returns a value in the range 1.0-0.0. The status and direction of the parent animation are also reversed. ReverseAnimation is immutable and stateless.

ProxyAnimation 子类接收一个 Animation<double> 类作为其父级,并仅转发该父级的当前状态。然而,父级是可变的。

The ProxyAnimation subclass takes an Animation<double> class as its parent and merely forwards the current state of that parent. However, the parent is mutable.

TrainHoppingAnimation 子类接收两个父类,并在它们的值交叉时在它们之间切换。

The TrainHoppingAnimation subclass takes two parents, and switches between them when their values cross.

动画控制器

Animation Controllers

AnimationController 是一个有状态的 Animation<double>,并使用一个 Ticker 来提供生命周期,它可以被启动和停止。每次运行,它会收集从启动开始经过的时间,并将其传递给 Simulation 来获得一个值,这就是在当前时间戳下它应该传递的值。如果 Simulation 反馈此时动画已经结束了,则控制器就会自行停止。

The AnimationController is a stateful Animation<double> that uses a Ticker to give itself life. It can be started and stopped. At each tick, it takes the time elapsed since it was started and passes it to a Simulation to obtain a value. That is then the value it reports. If the Simulation reports that at that time it has ended, then the controller stops itself.

可以给动画控制器设置动画运行的下限和上限,还有动画的持续时间。

The animation controller can be given a lower and upper bound to animate between, and a duration.

在一般情况下(使用 forward()reverse()play() 或者 resume()),动画控制器只是简单地在持续时间内线性地从下限至上限(反之亦然,用于在反向方向)进行插值补间。

In the simple case (using forward() or reverse()), the animation controller simply does a linear interpolation from the lower bound to the upper bound (or vice versa, for the reverse direction) over the given duration.

当使用 repeat() 时,动画控制器会在持续时间内线性地在上下边界之间进行插值补间,但会一直重复,不会停止。

When using repeat(), the animation controller uses a linear interpolation between the given bounds over the given duration, but does not stop.

当使用 animateTo() 时,动画控制器会在持续时间内线性地从当前值到给定目标值进行插值补间。如果方法没有指定持续时间,则使用控制器的默认持续时间和控制器的上下限范围来确定动画的速度。

When using animateTo(), the animation controller does a linear interpolation over the given duration from the current value to the given target. If no duration is given to the method, the default duration of the controller and the range described by the controller’s lower bound and upper bound is used to determine the velocity of the animation.

当使用 fling() 时,一个 Force 被用来创建一个特定的模拟器,然后用来驱动控制器。

When using fling(), a Force is used to create a specific simulation which is then used to drive the controller.

当使用 animateWith() 时,给定的模拟器会被用于驱动控制器。

When using animateWith(), the given simulation is used to drive the controller.

这些方法都会返回 Ticker 提供的将来值,交由控制器下一次停止或改变模拟器时来完成。

These methods all return the future that the Ticker provides and which will resolve when the controller next stops or changes simulation.

将 animatables 附加到动画上

Attaching animatables to animations

Animation<double>(新父级)传递给一个 Animatable 类的 animate() 方法将创建一个新的 Animation 子类,它的作用类似于 Animatable,但是由给定的父级驱动。

Passing an Animation<double> (the new parent) to an Animatable’s animate() method creates a new Animation subclass that acts like the Animatable but is driven from the given parent.

教程 | 在 Flutter 应用里实现动画效果

目录

本教程将讲解如何在 Flutter 中构建显式动画。我们先来介绍一些动画库中的基本概念,类和方法,然后列举五个动画示例。这些示例互相关联,展示了动画库的不同方面。

This tutorial shows you how to build explicit animations in Flutter. After introducing some of the essential concepts, classes, and methods in the animation library, it walks you through 5 animation examples. The examples build on each other, introducing you to different aspects of the animation library.

Flutter SDK 也内置了显式动画,比如 FadeTransitionSizeTransitionSlideTransition。这些简单的动画可以通过设置起点和终点来触发。它们比下面介绍的显式动画更容易实现。

The Flutter SDK also provides built-in explicit animations, such as FadeTransition, SizeTransition, and SlideTransition. These simple animations are triggered by setting a beginning and ending point. They are simpler to implement than custom explicit animations, which are described here.

基本动画概念和类

Essential animation concepts and classes

Flutter 中的动画系统基于类型化的 Animation 对象。 Widgets 既可以通过读取当前值和监听状态变化直接合并动画到 build 函数,也可以作为传递给其他 widgets 的更精细动画的基础。

The animation system in Flutter is based on typed Animation objects. Widgets can either incorporate these animations in their build functions directly by reading their current value and listening to their state changes or they can use the animations as the basis of more elaborate animations that they pass along to other widgets.

Animation<double>

在 Flutter 中,动画对象无法获取屏幕上显示的内容。 Animation 是一个已知当前值和状态(已完成或已解除)的抽象类。一个比较常见的动画类型是 Animation<double>

In Flutter, an Animation object knows nothing about what is onscreen. An Animation is an abstract class that understands its current value and its state (completed or dismissed). One of the more commonly used animation types is Animation<double>.

一个 Animation 对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation 对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation 对象的不同控制方式,它可以反向运行,或者中途切换方向。

An Animation object sequentially generates interpolated numbers between two values over a certain duration. The output of an Animation object might be linear, a curve, a step function, or any other mapping you can devise. Depending on how the Animation object is controlled, it could run in reverse, or even switch directions in the middle.

动画还可以插入除 double 以外的类型,比如 Animation<Color> 或者 Animation<Size>

Animations can also interpolate types other than double, such as Animation<Color> or Animation<Size>.

Animation 对象具有状态。它的当前值在 .value 中始终可用。

An Animation object has state. Its current value is always available in the .value member.

Animation 对象与渲染或 build() 函数无关。

An Animation object knows nothing about rendering or build() functions.

Curved­Animation

CurvedAnimation 定义动画进程为非线性曲线。

A CurvedAnimation defines the animation’s progress as a non-linear curve.

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationAnimationController(下面将会详细说明)都是 Animation<double> 类型,所以可以互换使用。 CurvedAnimation 封装正在修改的对象 — 不需要将 AnimationController 分解成子类来实现曲线。

CurvedAnimation and AnimationController (described in the next section) are both of type Animation<double>, so you can pass them interchangeably. The CurvedAnimation wraps the object it’s modifying—you don’t subclass AnimationController to implement a curve.

Animation­Controller

AnimationController 是个特殊的 Animation 对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController 在给定期间内会线性生成从 0.0 到 1.0 的数字。例如,这段代码创建了一个动画对象,但是没有启动运行。

AnimationController is a special Animation object that generates a new value whenever the hardware is ready for a new frame. By default, an AnimationController linearly produces the numbers from 0.0 to 1.0 during a given duration. For example, this code creates an Animation object, but does not start it running:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController 源自于 Animation<double>,所以可以用在任何需要 Animation 对象的地方。但是 AnimationController 还有其他方法控制动画。例如,使用 .forward() 方法启动动画。数字的生成与屏幕刷新关联,所以一般来说每秒钟会生成 60 个数字。数字生成之后,每个动画对象都调用附加 Listener 对象。为每个 child 创建自定义显示列表,请参考 RepaintBoundary

AnimationController derives from Animation<double>, so it can be used wherever an Animation object is needed. However, the AnimationController has additional methods to control the animation. For example, you start an animation with the .forward() method. The generation of numbers is tied to the screen refresh, so typically 60 numbers are generated per second. After each number is generated, each Animation object calls the attached Listener objects. To create a custom display list for each child, see RepaintBoundary.

创建 AnimationController 的同时,也赋予了一个 vsync 参数。 vsync 的存在防止后台动画消耗不必要的资源。您可以通过添加 SingleTickerProviderStateMixin 到类定义,将有状态的对象用作 vsync。可参考 GitHub 网站 animate1 中的示例。

When creating an AnimationController, you pass it a vsync argument. The presence of vsync prevents offscreen animations from consuming unnecessary resources. You can use your stateful object as the vsync by adding SingleTickerProviderStateMixin to the class definition. You can see an example of this in animate1 on GitHub.

Tween

在默认情况下,AnimationController 对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween 配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween 的范围是 -200 到 0.0。

By default, the AnimationController object ranges from 0.0 to 1.0. If you need a different range or a different data type, you can use a Tween to configure an animation to interpolate to a different range or data type. For example, the following Tween goes from -200.0 to 0.0:

tween = Tween<double>(begin: -200, end: 0);

Tween 是无状态的对象,只有 beginendTween 的这种单一用途用来定义从输入范围到输出范围的映射。输入范围一般为 0.0-1.0,但这并不是必须的。

A Tween is a stateless object that takes only begin and end. The sole job of a Tween is to define a mapping from an input range to an output range. The input range is commonly 0.0 to 1.0, but that’s not a requirement.

Tween 源自 Animatable<T>,而不是 Animation<T>。像动画这样的可动画元素不必重复输出。例如,ColorTween 指定了两种颜色之间的过程。

A Tween inherits from Animatable<T>, not from Animation<T>. An Animatable, like Animation, doesn’t have to output double. For example, ColorTween specifies a progression between two colors.

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 对象不存储任何状态。而是提供 evaluate(Animation<double> animation) 方法,将映射函数应用于动画当前值。 Animation 对象的当前值可以在 .value 方法中找到。 evaluate 函数还执行一些内部处理内容,比如确保当动画值在 0.0 和1.0 时分别返回起始点和终点。

A Tween object does not store any state. Instead, it provides the evaluate(Animation<double> animation) method that applies the mapping function to the current value of the animation. The current value of the Animation object can be found in the .value method. The evaluate function also performs some housekeeping, such as ensuring that begin and end are returned when the animation values are 0.0 and 1.0, respectively.

Tween.animate

要使用 Tween 对象,请在 Tween 调用 animate(),传入控制器对象。例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值。

To use a Tween object, call animate() on the Tween, passing in the controller object. For example, the following code generates the integer values from 0 to 255 over the course of 500 ms.

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

下面的示例展示了一个控制器,一个曲线,和一个 Tween

The following example shows a controller, a curve, and a Tween:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

动画通知

Animation notifications

一个 Animation 对象可以有不止一个 ListenerStatusListener,用 addListener()addStatusListener() 来定义。当动画值改变时调用 ListenerListener 最常用的操作是调用 setState() 进行重建。当一个动画开始,结束,前进或后退时,会调用 StatusListener,用 AnimationStatus 来定义。下一部分有关于 addListener() 方法的示例,在 监控动画过程 中也有 addStatusListener() 的示例。

An Animation object can have Listeners and StatusListeners, defined with addListener() and addStatusListener(). A Listener is called whenever the value of the animation changes. The most common behavior of a Listener is to call setState() to cause a rebuild. A StatusListener is called when an animation begins, ends, moves forward, or moves reverse, as defined by AnimationStatus. The next section has an example of the addListener() method, and Monitoring the progress of the animation shows an example of addStatusListener().


动画示例

Animation examples

这部分列举了五个动画示例,每个示例都提供了源代码的链接。

This section walks you through 5 animation examples. Each section provides a link to the source code for that example.

渲染动画

Rendering animations

目前为止,我们学习了如何随着时间生成数字序列。但屏幕上并未显示任何内容。要显示一个 Animation 对象,需将 Animation 对象存储为您的 widget 成员,然后用它的值来决定如何绘制。

So far you’ve learned how to generate a sequence of numbers over time. Nothing has been rendered to the screen. To render with an Animation object, store the Animation object as a member of your widget, then use its value to decide how to draw.

参考下面的应用程序,它没有使用动画绘制 Flutter logo。

Consider the following app that draws the Flutter logo without animation:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class LogoApp extends StatefulWidget {
  const LogoApp({Key? key}) : super(key: key);

  @override
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: const FlutterLogo(),
      ),
    );
  }
}

源代码: animate0

App source: animate0

下面的代码是加入动画效果的,logo 从无到全屏。当定义 AnimationController 时,必须要使用一个 vsync 对象。在 AnimationController 部分 会具体介绍 vsync 参数。

The following shows the same code modified to animate the logo to grow from nothing to full size. When defining an AnimationController, you must pass in a vsync object. The vsync parameter is described in the AnimationController section.

对比无动画示例,改动部分被突出显示:

The changes from the non-animated example are highlighted:

{animate0 → animate1}/lib/main.dart
@@ -9,16 +9,39 @@
9
9
  _LogoAppState createState() => _LogoAppState();
10
10
  }
11
- class _LogoAppState extends State<LogoApp> {
11
+ class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
12
+ late Animation<double> animation;
13
+ late AnimationController controller;
14
+
15
+ @override
16
+ void initState() {
17
+ super.initState();
18
+ controller =
19
+ AnimationController(duration: const Duration(seconds: 2), vsync: this);
20
+ animation = Tween<double>(begin: 0, end: 300).animate(controller)
21
+ ..addListener(() {
22
+ setState(() {
23
+ // The state that has changed here is the animation object’s value.
24
+ });
25
+ });
26
+ controller.forward();
27
+ }
28
+
12
29
  @override
13
30
  Widget build(BuildContext context) {
14
31
  return Center(
15
32
  child: Container(
16
33
  margin: const EdgeInsets.symmetric(vertical: 10),
17
- height: 300,
18
- width: 300,
34
+ height: animation.value,
35
+ width: animation.value,
19
36
  child: const FlutterLogo(),
20
37
  ),
21
38
  );
22
39
  }
40
+
41
+ @override
42
+ void dispose() {
43
+ controller.dispose();
44
+ super.dispose();
45
+ }
23
46
  }

源代码: animate1

App source: animate1

因为addListener() 函数调用 setState(),所以每次 Animation 生成一个新的数字,当前帧就被标记为 dirty,使得 build() 再次被调用。在 build() 函数中,container 会改变大小,因为它的高和宽都读取 animation.value,而不是固定编码值。当 State 对象销毁时要清除控制器以防止内存溢出。

The addListener() function calls setState(), so every time the Animation generates a new number, the current frame is marked dirty, which forces build() to be called again. In build(), the container changes size because its height and width now use animation.value instead of a hardcoded value. Dispose of the controller when the State object is discarded to prevent memory leaks.

经过这些小改动,你成功创建了第一个 Flutter 动画。

With these few changes, you’ve created your first animation in Flutter!

使用 Animated­Widget 进行简化

Simplifying with Animated­Widget

AnimatedWidget 基本类可以从动画代码中区分出核心 widget 代码。 AnimatedWidget 不需要保持 State 对象来 hold 动画。可以添加下面的 AnimatedLogo 类:

The AnimatedWidget base class allows you to separate out the core widget code from the animation code. AnimatedWidget doesn’t need to maintain a State object to hold the animation. Add the following AnimatedLogo class:

lib/main.dart (AnimatedLogo)
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

在绘制时,AnimatedLogo 会读取 animation 当前值。

AnimatedLogo uses the current value of the animation when drawing itself.

LogoApp 持续控制 AnimationControllerTween,并将 Animation 对象传给 AnimatedLogo

The LogoApp still manages the AnimationController and the Tween, and it passes the Animation object to AnimatedLogo:

{animate1 → animate2}/lib/main.dart
@@ -1,10 +1,28 @@
1
1
  import 'package:flutter/material.dart';
2
2
  void main() => runApp(const LogoApp());
3
+ class AnimatedLogo extends AnimatedWidget {
4
+ const AnimatedLogo({Key? key, required Animation<double> animation})
5
+ : super(key: key, listenable: animation);
6
+
7
+ @override
8
+ Widget build(BuildContext context) {
9
+ final animation = listenable as Animation<double>;
10
+ return Center(
11
+ child: Container(
12
+ margin: const EdgeInsets.symmetric(vertical: 10),
13
+ height: animation.value,
14
+ width: animation.value,
15
+ child: const FlutterLogo(),
16
+ ),
17
+ );
18
+ }
19
+ }
20
+
3
21
  class LogoApp extends StatefulWidget {
4
22
  const LogoApp({Key? key}) : super(key: key);
5
23
  @override
6
24
  _LogoAppState createState() => _LogoAppState();
7
25
  }
@@ -15,32 +33,18 @@
15
33
  @override
16
34
  void initState() {
17
35
  super.initState();
18
36
  controller =
19
37
  AnimationController(duration: const Duration(seconds: 2), vsync: this);
20
- animation = Tween<double>(begin: 0, end: 300).animate(controller)
21
- ..addListener(() {
22
- setState(() {
23
- // The state that has changed here is the animation object’s value.
24
- });
25
- });
38
+ animation = Tween<double>(begin: 0, end: 300).animate(controller);
26
39
  controller.forward();
27
40
  }
28
41
  @override
29
- Widget build(BuildContext context) {
30
- return Center(
31
- child: Container(
32
- margin: const EdgeInsets.symmetric(vertical: 10),
33
- height: animation.value,
34
- width: animation.value,
35
- child: const FlutterLogo(),
36
- ),
37
- );
38
- }
42
+ Widget build(BuildContext context) => AnimatedLogo(animation: animation);
39
43
  @override
40
44
  void dispose() {
41
45
  controller.dispose();
42
46
  super.dispose();
43
47
  }

源代码: animate2

App source: animate2

监控动画过程

Monitoring the progress of the animation

了解动画何时改变状态通常是很有用的,比如完成,前进或后退。可以通过 addStatusListener() 来获得提示。下面是之前示例修改后的代码,这样就可以监听状态的改变和更新。修改部分会突出显示:

It’s often helpful to know when an animation changes state, such as finishing, moving forward, or reversing. You can get notifications for this with addStatusListener(). The following code modifies the previous example so that it listens for a state change and prints an update. The highlighted line shows the change:

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((state) => print('$state'));
    controller.forward();
  }
  // ...
}

运行这段代码,得到如下结果:

Running this code produces this output:

AnimationStatus.forward
AnimationStatus.completed

下一步,在起始或结束时,使用 addStatusListener() 反转动画。制造“呼吸”效果:

Next, use addStatusListener() to reverse the animation at the beginning or the end. This creates a “breathing” effect:

{animate2 → animate3}/lib/main.dart
@@ -35,7 +35,15 @@
35
35
  void initState() {
36
36
  super.initState();
37
37
  controller =
38
38
  AnimationController(duration: const Duration(seconds: 2), vsync: this);
39
- animation = Tween<double>(begin: 0, end: 300).animate(controller);
39
+ animation = Tween<double>(begin: 0, end: 300).animate(controller)
40
+ ..addStatusListener((status) {
41
+ if (status == AnimationStatus.completed) {
42
+ controller.reverse();
43
+ } else if (status == AnimationStatus.dismissed) {
44
+ controller.forward();
45
+ }
46
+ })
47
+ ..addStatusListener((state) => print('$state'));
40
48
  controller.forward();
41
49
  }

源代码: animate3

App source: animate3

使用 AnimatedBuilder 进行重构

Refactoring with AnimatedBuilder

animate3 示例代码中有个问题,就是改变动画需要改变渲染 logo 的widget。较好的解决办法是,将任务区分到不同类里:

One problem with the code in the animate3 example, is that changing the animation required changing the widget that renders the logo. A better solution is to separate responsibilities into different classes:

您可以使用 AnimatedBuilder 类方法来完成分配。 AnimatedBuilder 作为渲染树的一个单独类。像 AnimatedWidgetAnimatedBuilder 自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用 addListener()

You can accomplish this separation with the help of the AnimatedBuilder class. An AnimatedBuilder is a separate class in the render tree. Like AnimatedWidget, AnimatedBuilder automatically listens to notifications from the Animation object, and marks the widget tree dirty as necessary, so you don’t need to call addListener().

应用于 animate4 示例的 widget 树长这样:

The widget tree for the animate4 example looks like this:

AnimatedBuilder widget tree

从 widget 树底部开始,渲染 logo 的代码很容易:

Starting from the bottom of the widget tree, the code for rendering the logo is straightforward:

class LogoWidget extends StatelessWidget {
  const LogoWidget({Key? key}) : super(key: key);

  // Leave out the height and width so it fills the animating parent
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}

图表中间的三部分都是用 GrowTransition 中的 build() 方法创建的,如下。 GrowTransition widget 本身是无状态的,而且拥有定义过渡动画所需的一系列最终变量。 build() 函数创建并返回 AnimatedBuilderAnimatedBuilder 使用(Anonymous builder)方法并将 LogoWidget 对象作为参数。渲染过渡效果实际上是在(Anonymous builder)方法中完成的,该方法创建一个适当大小 Container 强制 LogoWidget 配合。

The middle three blocks in the diagram are all created in the build() method in GrowTransition, shown below. The GrowTransition widget itself is stateless and holds the set of final variables necessary to define the transition animation. The build() function creates and returns the AnimatedBuilder, which takes the (Anonymous builder) method and the LogoWidget object as parameters. The work of rendering the transition actually happens in the (Anonymous builder) method, which creates a Container of the appropriate size to force the LogoWidget to shrink to fit.

在下面这段代码中,一个比较棘手的问题是 child 看起来被指定了两次。其实是 child 的外部参照被传递给了 AnimatedBuilder,再传递给匿名闭包,然后用作 child 的对象。最终结果就是 AnimatedBuilder 被插入渲染树的两个 widgets 中间。

One tricky point in the code below is that the child looks like it’s specified twice. What’s happening is that the outer reference of child is passed to AnimatedBuilder, which passes it to the anonymous closure, which then uses that object as its child. The net result is that the AnimatedBuilder is inserted in between the two widgets in the render tree.

class GrowTransition extends StatelessWidget {
  const GrowTransition({required this.child, required this.animation, Key? key})
      : super(key: key);

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

最后,初始动画的代码看起来很像 animate2 的示例。 initState() 方法创建了 AnimationControllerTween,然后用 animate() 绑定它们。神奇的是 build() 方法,它返回一个以LogoWidget 为 child 的 GrowTransition 对象,和一个驱动过渡的动画对象。上面列出了三个主要因素。

Finally, the code to initialize the animation looks very similar to the animate2 example. The initState() method creates an AnimationController and a Tween, then binds them with animate(). The magic happens in the build() method, which returns a GrowTransition object with a LogoWidget as a child, and an animation object to drive the transition. These are the three elements listed in the bullet points above.

{animate2 → animate4}/lib/main.dart
@@ -1,27 +1,47 @@
1
1
  import 'package:flutter/material.dart';
2
2
  void main() => runApp(const LogoApp());
3
- class AnimatedLogo extends AnimatedWidget {
4
- const AnimatedLogo({Key? key, required Animation<double> animation})
5
- : super(key: key, listenable: animation);
3
+ class LogoWidget extends StatelessWidget {
4
+ const LogoWidget({Key? key}) : super(key: key);
5
+
6
+ // Leave out the height and width so it fills the animating parent
7
+ @override
8
+ Widget build(BuildContext context) {
9
+ return Container(
10
+ margin: const EdgeInsets.symmetric(vertical: 10),
11
+ child: const FlutterLogo(),
12
+ );
13
+ }
14
+ }
15
+
16
+ class GrowTransition extends StatelessWidget {
17
+ const GrowTransition({required this.child, required this.animation, Key? key})
18
+ : super(key: key);
19
+
20
+ final Widget child;
21
+ final Animation<double> animation;
6
22
  @override
7
23
  Widget build(BuildContext context) {
8
- final animation = listenable as Animation<double>;
9
24
  return Center(
10
- child: Container(
11
- margin: const EdgeInsets.symmetric(vertical: 10),
12
- height: animation.value,
13
- width: animation.value,
14
- child: const FlutterLogo(),
25
+ child: AnimatedBuilder(
26
+ animation: animation,
27
+ builder: (context, child) {
28
+ return SizedBox(
29
+ height: animation.value,
30
+ width: animation.value,
31
+ child: child,
32
+ );
33
+ },
34
+ child: child,
15
35
  ),
16
36
  );
17
37
  }
18
38
  }
19
39
  class LogoApp extends StatefulWidget {
20
40
  const LogoApp({Key? key}) : super(key: key);
21
41
  @override
22
42
  _LogoAppState createState() => _LogoAppState();
@@ -34,18 +54,23 @@
34
54
  @override
35
55
  void initState() {
36
56
  super.initState();
37
57
  controller =
38
58
  AnimationController(duration: const Duration(seconds: 2), vsync: this);
39
59
  animation = Tween<double>(begin: 0, end: 300).animate(controller);
40
60
  controller.forward();
41
61
  }
42
62
  @override
43
- Widget build(BuildContext context) => AnimatedLogo(animation: animation);
63
+ Widget build(BuildContext context) {
64
+ return GrowTransition(
65
+ child: const LogoWidget(),
66
+ animation: animation,
67
+ );
68
+ }
44
69
  @override
45
70
  void dispose() {
46
71
  controller.dispose();
47
72
  super.dispose();
48
73
  }
49
74
  }

源代码: animate4

App source: animate4

同步动画

Simultaneous animations

在这部分内容中,您会根据 监控动画过程 (animate3) 创建示例,该示例将使用 AnimatedWidget 持续进行动画。可以用在需要对透明度进行从透明到不透明动画处理的情况。

In this section, you’ll build on the example from monitoring the progress of the animation (animate3), which used AnimatedWidget to animate in and out continuously. Consider the case where you want to animate in and out while the opacity animates from transparent to opaque.

每个补间动画控制一个动画的不同方面,例如:

Each tween manages an aspect of the animation. For example:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);

通过 sizeAnimation.value 我们可以得到尺寸,通过 opacityAnimation.value 可以得到不透明度,但是 AnimatedWidget 的构造函数只读取单一的 Animation 对象。为了解决这个问题,该示例创建了一个 Tween 对象并计算确切值。

You can get the size with sizeAnimation.value and the opacity with opacityAnimation.value, but the constructor for AnimatedWidget only takes a single Animation object. To solve this problem, the example creates its own Tween objects and explicitly calculates the values.

修改 AnimatedLogo 来封装其 Tween 对象,以及其 build() 方法在母动画对象上调用 Tween.evaluate() 来计算所需的尺寸和不透明度值。下面的代码中将这些改动突出显示:

Change AnimatedLogo to encapsulate its own Tween objects, and its build() method calls Tween.evaluate() on the parent’s animation object to calculate the required size and opacity values. The following code shows the changes with highlights:

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

  // Make the Tweens static because they don't change.
  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: const EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  const LogoApp({Key? key}) : super(key: key);

  @override
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

源代码: animate5

App source: animate5

下面的步骤

Next steps

本指南是在 Flutter 中应用 Tweens 创建动画的基础介绍,还有很多其他类可供探索。比如指定 Tween 类,Material Design 特有的动画, ReverseAnimation,共享元素过渡(也称为 Hero 动画),物理模拟和 fling() 方法。关于最新的文档和示例可参见 动画效果介绍

This tutorial gives you a foundation for creating animations in Flutter using Tweens, but there are many other classes to explore. You might investigate the specialized Tween classes, animations specific to Material Design, ReverseAnimation, shared element transitions (also known as Hero animations), physics simulations and fling() methods. See the animations landing page for the latest available documents and examples.

隐式动画

目录

通过 Flutter 的 动画库,你可以为 UI 中的 widgets 添加动作并创造视觉效果。有些库包含各种各样可以帮你管理动画的 widget。这些 widgets 被统称为 隐式动画隐式动画 widget,其名字来源于它们所实现的 ImplicitlyAnimatedWidget 类。下列资源提供了许多在 Flutter 中学习使用隐式动画的方法。

With Flutter’s animation library, you can add motion and create visual effects for the widgets in your UI. One part of the library is an assortment of widgets that manage animations for you. These widgets are collectively referred to as implicit animations, or implicitly animated widgets, deriving their name from the ImplicitlyAnimatedWidget class that they implement. The following set of resources provide many ways to learn about implicit animations in Flutter.

文档

Documentation

隐式动画 codelab
跳转至代码! Codelab 使用交互式示例和分布介绍来教你学会如何使用隐式动画。

Implicit animations codelab
Jump right into the code! This codelab uses interactive examples and step-by-step instructions to teach you how to use implicit animations.

AnimatedContainer 示例
Flutter cookbook 中针对如何使用 AnimatedContainer 隐式动画 widget 进行了手把手的指导。

AnimatedContainer sample
A step-by-step recipe from the Flutter cookbook for using the AnimatedContainer implicitly animated widget.

ImplicitlyAnimatedWidget API 页面
所有隐式动画都扩展了 ImplicitlyAnimatedWidget 类。

ImplicitlyAnimatedWidget API page
All implicit animations extend the ImplicitlyAnimatedWidget class.

聚焦 Flutter 视频

Flutter in Focus videos

聚焦 Flutter 视频以 5 到 10 分钟的实战代码为特点,涵盖了每个 Flutter 开发人员都需要从头到尾了解的技术。下列视频涵盖了所有与隐性动画相关的话题。

Flutter in Focus videos feature 5-10 minute tutorials with real code that cover techniques that every Flutter dev needs to know from top to bottom. The following videos cover topics that are relevant to implicit animations.

The Boring Show

观看《The Boring Show》,跟随谷歌工程师用 Flutter 从零开始构建应用程序。下面这一集涉及在一个新闻聚合器应用中使用隐式动画。

Watch the Boring Show to follow Google Engineers build apps from scratch in Flutter. The following episode covers using implicit animations in a news aggregator app.

每周 Widget 视频

Widget of the Week videos

每周都有一个系列的动画短片,每个短片都展示了一个特定 widget 的核心功能。在大约六十秒的时间里,你将会看到每个 widget 的实战代码,以及关于它是如何工作的演示。下列「每周 Widget」视频涉及了隐含动画 widget 有:

A weekly series of short animated videos each showing the important features of one particular widget. In about 60 seconds, you’ll see real code for each widget with a demo about how it works. The following Widget of the Week videos cover implicitly animated widgets:

主动画 (Hero animations)

目录

你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个页面称为 hero 动画,相同的动作有时也被称为 共享元素过渡

You’ve probably seen hero animations many times. For example, a screen displays a list of thumbnails representing items for sale. Selecting an item flies it to a new screen, containing more details and a “Buy” button. Flying an image from one screen to another is called a hero animation in Flutter, though the same motion is sometimes referred to as a shared element transition.

下面的一分钟视频介绍了 Hero widget:

You might want to watch this one-minute video introducing the Hero widget:

这个指南演示了如何创建标准 hero 动画,以及 hero 动画如何在飞行过程中将图像形状由圆形变成正方形。

This guide demonstrates how to build standard hero animations, and hero animations that transform the image from a circular shape to a square shape during flight.

您可以在 Flutter 中使用 Hero widgets 创建这个动画。当 hero 动画从原页面到目标页面,目标页面(减去 hero)淡入视野。可以说,heroes 是 UI 的一小部分,就像图像,两个页面有共同之处。从用户的角度来说,hero 在页面间「飞翔」。本指南展示如何创建如下 hero 动画:

You can create this animation in Flutter with Hero widgets. As the hero animates from the source to the destination route, the destination route (minus the hero) fades into view. Typically, heroes are small parts of the UI, like images, that both routes have in common. From the user’s perspective the hero “flies” between the routes. This guide shows how to create the following hero animations:

标准 hero 动画

Standard hero animations

一个 标准 hero 动画 使 hero 从一页飞至新页面,通常以不同大小到达不同的目的地。

A standard hero animation flies the hero from one route to a new route, usually landing at a different location and with a different size.

下面的视频(慢放)演示了一个典型示例。点击页面中间的 flippers,它将飞至一个新的蓝色页面的左上角,并缩小。点击蓝色页面中的 flippers(或者使用设备的回到前页手势),它将返回原页面。

The following video (recorded at slow speed) shows a typical example. Tapping the flippers in the center of the route flies them to the upper left corner of a new, blue route, at a smaller size. Tapping the flippers in the blue route (orusing the device’s back-to-previous-route gesture) flies the flippers back to the original route.

径向 hero 动画


Radial hero animations

径向 hero 动画 中,随着 hero 在页面间飞翔,它的形状也会有圆形变成矩形。

In radial hero animation, as the hero flies between routes its shape appears to change from circular to rectangular.

下面的视频(慢放)演示了一个径向 hero 动画的示例。开始,一排三个圆形的图像在页面底部。点击任意圆形图像,其飞至新页面,并变成正方形。点击正方形图像,hero 返回至原页面,并变回圆形。

The following video (recorded at slow speed), shows an example of a radial hero animation. At the start, a row of three circular images appears at the bottom of the route. Tapping any of the circular images flies that image to a new route that displays it with a square shape. Tapping the square image flies the hero back to the original route, displayed with a circular shape.

在学习 标准径向 hero 动画之前,请阅读 hero 动画基本结构 来学习如何构建 hero 动画代码,以及 幕后 来了解 Flutter 如何显示一个 hero 动画。


Before moving to the sections specific to standard or radial hero animations, read basic structure of a hero animation to learn how to structure hero animation code, and behind the scenes to understand how Flutter performs a hero animation.

hero 动画基本结构

Basic structure of a hero animation

Hero 动画需要使用两个 Hero widgets 来实现:一个用来在原页面中描述 widget,另一个在目标页面中描述 widget。从用户角度来说,hero 似乎是分享的,只有程序员需要了解实施细节。

Hero animations are implemented using two Hero widgets: one describing the widget in the source route, and another describing the widget in the destination route. From the user’s point of view, the hero appears to be shared, and only the programmer needs to understand this implementation detail.

Hero 动画代码有如下结构:

Hero animation code has the following structure:

  1. 定义一个起始 Hero widget,被称为 source hero。该 hero 指定图形表示(通常是图像),以及识别标签,并且在由原页面定义的当前显示的 widget 树中。

    Define a starting Hero widget, referred to as the source hero. The hero specifies its graphical representation (typically an image), and an identifying tag, and is in the currently displayed widget tree as defined by the source route.

  2. 定义一个截至 Hero widget,被称为 destination hero。该 hero 也指定图形表示,并与 source hero 使用同样的标签。 这是基本,两个 hero widgets 要创建相同的标签,通常是代表基础数据的对象。为了获得最佳效果,heroes 应该有几乎完全相同的 widget 树。

    Define an ending Hero widget, referred to as the destination hero. This hero also specifies its graphical representation, and the same tag as the source hero. It’s essential that both hero widgets are created with the same tag, typically an object that represents the underlying data. For best results, the heroes should have virtually identical widget trees.

  3. 创建一个含有 destination hero 的页面。目标页面定义了动画结束时应有的 widget 树。

    Create a route that contains the destination hero. The destination route defines the widget tree that exists at the end of the animation.

  4. 通过推送目标页面到 Navigator 堆栈来触发动画。 Navigator 推送并弹出操作触发原页面和目标页面中含有配对标签 heroes 的 hero 动画。

    Trigger the animation by pushing the destination route on the Navigator’s stack. The Navigator push and pop operations trigger a hero animation for each pair of heroes with matching tags in the source and destination routes.

Flutter 设置了 tween 用来界定 Hero 从起点到终点的界限(插入大小和位置),并在图层上执行动画。

Flutter calculates the tween that animates the Hero’s bounds from the starting point to the endpoint (interpolating size and position), and performs the animation in an overlay.

下一章节将更详细地介绍 Flutter 的过程。

The next section describes Flutter’s process in greater detail.

幕后

Behind the scenes

下面将介绍 Flutter 如何执行一个页面到另一页面的过渡。

在过渡之前 source hero 出现在原页面中

The following describes how Flutter performs the transition from one route to another.

过渡前,source hero 在原页面的 widget 树中等待。而目标页面此时并不存在,图层也是空的。

Before transition, the source hero waits in the source route’s widget tree. The destination route does not yet exist, and the overlay is empty.


过渡开始

推送一个页面到 Navigator 来触发动画。t=0.0 时,Flutter 执行如下动作:

Pushing a route to the Navigator triggers the animation. At t=0.0, Flutter does the following:


The hero flies in the overlay to its final position and size (hero 飞入图层到达其最终位置和大小)

hero 飞翔时,它的矩形边界使用 Hero 的 createRectTween 属性中特定的 Tween<Rect> 进行动画。默认情况下,Flutter 使用 MaterialRectArcTween 的示例,它沿着一个曲线路径设置矩形对角动画。(参考 径向 hero 动画,该示例使用了不同的补间动画)

As the hero flies, its rectangular bounds are animated using Tween<Rect>, specified in Hero’s createRectTween property. By default, Flutter uses an instance of MaterialRectArcTween, which animates the rectangle’s opposing corners along a curved path. (See Radial hero animations for an example that uses a different Tween animation.)


When the transition is complete, the hero is moved from the overlay to the destination route (当过渡完成时,hero 从图层移动到目的页面)

当飞翔完成时:

When the flight completes:


弹出的页面执行同样的过程,hero 动画回到原页面并回复原来大小和位置。

Popping the route performs the same process, animating the hero back to its size and location in the source route.

基本类

Essential classes

本指南中的示例使用了如下类来实现 hero 动画:

The examples in this guide use the following classes to implement hero animations:

Hero
从原页面飞到目标页面的 widget。定义一个原页面的 Hero 和另一个目标页面的 Hero,并设置相同的标签。 Flutter 为成对的含有匹配标签的 heroes 设置动画。

Hero
The widget that flies from the source to the destination route. Define one Hero for the source route and another for the destination route, and assign each the same tag. Flutter animates pairs of heroes with matching tags.

Inkwell
指定点击 hero 时发生什么。 InkWell 的 onTap() 方法可以创建新页面并推送至 Navigator 的堆栈。

Inkwell
Specifies what happens when tapping the hero. The InkWell’s onTap() method builds the new route and pushes it to the Navigator’s stack.

Navigator
Navigator 管理一个页面堆栈。推送或弹出 Navigator 堆栈中的页面触发动画。

Navigator
The Navigator manages a stack of routes. Pushing a route on or popping a route from the Navigator’s stack triggers the animation.

Route
指定屏幕或页面。除最基本的应用程序外,大部分含有多页面。

Route
Specifies a screen or page. Most apps, beyond the most basic, have multiple routes.

标准 hero 动画

Standard hero animations

然后呢?

What’s going on?

使用 Flutter 的 hero widget 可以轻松实现图像由一个页面飞至另一个。当使用 MaterialPageRoute 指定新页面时,图像将沿 Material Design motion spec 中介绍的曲线路径飞翔。

Flying an image from one route to another is easy to implement using Flutter’s hero widget. When using MaterialPageRoute to specify the new route, the image flies along a curved path, as described by the Material Design motion spec.

创建一个新的 Flutter 示例 并使用来自 hero_animation 的文件更新。

Create a new Flutter example and update it using the files from the hero_animation.

运行示例:

To run the example:

PhotoHero 类

PhotoHero class

自定义的 PhotoHero 类保留了 hero 以及其大小,图像,和点击时的动作。PhotoHero 创建如下 widget 树:

The custom PhotoHero class maintains the hero, and its size, image, and behavior when tapped. The PhotoHero builds the following widget tree:

PhotoHero class widget tree

代码如下:

Here’s the code:

class PhotoHero extends StatelessWidget {
  const PhotoHero({ Key key, this.photo, this.onTap, this.width }) : super(key: key);

  final String photo;
  final VoidCallback onTap;
  final double width;

  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Image.asset(
              photo,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

重要信息:

Key information:

HeroAnimation 类

HeroAnimation class

HeroAnimation 类可以创建 source PhotoHero 和 destination PhotoHero,并建立过渡。

The HeroAnimation class creates the source and destination PhotoHeroes, and sets up the transition.

代码如下:

Here’s the code:

class HeroAnimation extends StatelessWidget {
  Widget build(BuildContext context) {
    timeDilation = 5.0; // 1.0 means normal animation speed.

    return Scaffold(
      appBar: AppBar(
        title: const Text('Basic Hero Animation'),
      ),
      body: Center(
        child: PhotoHero(
          photo: 'images/flippers-alpha.png',
          width: 300.0,
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute<void>(
              builder: (BuildContext context) {
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Flippers Page'),
                  ),
                  body: Container(
                    // The blue background emphasizes that it's a new route.
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16.0),
                    alignment: Alignment.topLeft,
                    child: PhotoHero(
                      photo: 'images/flippers-alpha.png',
                      width: 100.0,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                );
              }
            ));
          },
        ),
      ),
    );
  }
}

重要信息:

Key information:


径向 hero 动画

Radial hero animations

hero 从一个页面飞至另一页的同时由圆形过渡到矩形,这是一个滑入效果,可使用 Hero widgets 来实现。要做到这一点,代码需要动画两个剪裁形状的交叉:一个圆形和一个正方形。整个动画中,圆形剪裁(和图片)由 minRadius 缩放到 maxRadius,而正方形剪裁保持大小不变。同时,图像从原页面飞至目标页面的相同位置。这个过渡的效果示例,请参见 Material motion spec 中的 径向过渡

Flying a hero from one route to another as it transforms from a circular shape to a rectangular shape is a slick effect that you can implement using Hero widgets. To accomplish this, the code animates the intersection of two clip shapes: a circle and a square. Throughout the animation, the circle clip (and the image) scales from minRadius to maxRadius, while the square clip maintains constant size. At the same time, the image flies from its position in the source route to its position in the destination route. For visual examples of this transition, see Radial transformation in the Material motion spec.

这个动画看起来复杂,但是您可以根据自身需要自定义范例。艰巨的工作已为您完成。

This animation might seem complex (and it is), but you can customize the provided example to your needs. The heavy lifting is done for you.

然后呢?

What’s going on?

下面的图表显示了在动画起始(t = 0.0)和结束(t = 1.0)时的剪裁图像。

The following diagram shows the clipped image at the beginning (t = 0.0), and the end (t = 1.0) of the animation.

Radial transformation from beginning to end

蓝色渐变(代表图像),表明剪裁形状交叉的位置。在过渡的开始,交叉的结果是圆形剪裁 (ClipOval)。在过渡过程中,ClipOval 由 minRadius 缩放至 maxRadiusClipRect 则保持原尺寸。在过渡结束时,圆形和矩形剪裁的交集产生一个与 hero widget 相同大小的矩形。也就是说,在过渡结束时,图片已不再被剪裁。

The blue gradient (representing the image), indicates where the clip shapes intersect. At the beginning of the transition, the result of the intersection is a circular clip (ClipOval). During the transformation, the ClipOval scales from minRadius to maxRadius while the ClipRect maintains a constant size. At the end of the transition the intersection of the circular and rectangular clips yield a rectangle that’s the same size as the hero widget. In other words, at the end of the transition the image is no longer clipped.

创建一个新的 Flutter 示例 并使用来自 radial_hero_animation 的文件更新。

Create a new Flutter example and update it using the files from the radial_hero_animation GitHub directory.

运行示例:

To run the example:

Photo 类

Photo class

Photo 类创建保存图像的 widget 树:

The Photo class builds the widget tree that holds the image:

class Photo extends StatelessWidget {
  Photo({ Key key, this.photo, this.color, this.onTap }) : super(key: key);

  final String photo;
  final Color color;
  final VoidCallback onTap;

  Widget build(BuildContext context) {
    return Material(
      // Slightly opaque color appears where the image has transparency.
      color: Theme.of(context).primaryColor.withOpacity(0.25),
      child: InkWell(
        onTap: onTap,
        child: Image.asset(
            photo,
            fit: BoxFit.contain,
          )
      ),
    );
  }
}

重要信息:

Key information:

RadialExpansion 类

RadialExpansion class

RadialExpansion widget,demo 的核心,建立过渡过程中剪裁图像的 widget 树。剪裁的形状来自于圆形剪裁(飞翔过程中增长)和矩形剪裁(自始至终保持一致大小)的交集。

The RadialExpansion widget, the core of the demo, builds the widget tree that clips the image during the transition. The clipped shape results from the intersection of a circular clip (that grows during flight), with a rectangular clip (that remains a constant size throughout).

为此,它建立了如下 widget 树:

To do this, it builds the following widget tree:

RadialExpansion widget tree

代码如下:

Here’s the code:

class RadialExpansion extends StatelessWidget {
  RadialExpansion({
    Key key,
    this.maxRadius,
    this.child,
  }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2),
       super(key: key);

  final double maxRadius;
  final clipRectSize;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect(
            child: child,  // Photo
          ),
        ),
      ),
    );
  }
}

重要信息:

Key information:

交织动画

目录

交织动画是一个简单的概念:视觉变化是随着一系列的动作发生,而不是一次性的动作。动画可能是纯粹顺序的,一个改变随着一个改变发生,动画也可能是部分或者全部重叠的。动画也可能有间隙,没有变化发生。

Staggered animations are a straightforward concept: visual changes happen as a series of operations, rather than all at once. The animation might be purely sequential, with one change occurring after the next, or it might partially or completely overlap. It might also have gaps, where no changes occur.

本指南展示如何在Flutter中构建交织动画。

This guide shows how to build a staggered animation in Flutter.

以下视频演示了 basic_staggered_animation 所执行的动画:

The following video demonstrates the animation performed by basic_staggered_animation:

在这个视频中,你可以看到一个独立的 widget 的以下动画,以一个带边框的略微有圆角的蓝色矩形开始,这个矩形会按照以下顺序变化:

In the video, you see the following animation of a single widget, which begins as a bordered blue square with slightly rounded corners. The square runs through changes in the following order:

  1. 淡出

    Fades in

  2. 扩大

    Widens

  3. 向上移动同时变得更高

    Becomes taller while moving upwards

  4. 变为一个有边框的圆圈

    Transforms into a bordered circle

  5. 颜色变为橙色

    Changes color to orange

向前运行之后,动画将反向运行。

After running forward, the animation runs in reverse.

一个交织动画的基础结构

Basic structure of a staggered animation

下图展示了在 basic_staggered_animation 使用间隔的例子。你会注意到有以下特点:

The following diagram shows the Intervals used in the basic_staggered_animation example. You might notice the following characteristics:

Diagram showing the interval specified for each motion

设置这个动画:

To set up the animation:

当控制动画的值发生变化时,新动画的值也随之变化值更改,触发 UI 更新。

When the controlling animation’s value changes, the new animation’s value changes, triggering the UI to update.

下面的代码为 width 属性创建了一个 tween。

The following code creates a tween for the width property.

它创建了一个 CurvedAnimation, 指定一个 eased curve。其他更多的预定的动画曲线请看 Curves

It builds a CurvedAnimation, specifying an eased curve. See Curves for other available pre-defined animation curves.

width = Tween<double>(
  begin: 50.0,
  end: 150.0,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.125, 0.250,
      curve: Curves.ease,
    ),
  ),
),

beginend 的值不一定是 doubles。

The begin and end values don’t have to be doubles.

下面的代码为 borderRadius 属性创建一个 tween(控制矩形的圆角半径),使用 BorderRadius.circular()

The following code builds the tween for the borderRadius property (which controls the roundness of the square’s corners), using BorderRadius.circular().

borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4.0),
  end: BorderRadius.circular(75.0),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.375, 0.500,
      curve: Curves.ease,
    ),
  ),
),

完整的交织动画

Complete staggered animation

像所有可交互的 widgets 一样,完整的动画包括一对 widget:一个无状态 widget 和一个有状态的 widget。

Like all interactive widgets, the complete animation consists of a widget pair: a stateless and a stateful widget.

无状态 widget 指定 Tweens,定义动画对象,提供一个 build() 方法,负责构建 widget 树的动画部分。

The stateless widget specifies the Tweens, defines the Animation objects, and provides a build() function responsible for building the animating portion of the widget tree.

有状态 widget 创建控制器,播放动画,同时构建 widget 树的非动画部分。当在屏幕上检测到一个点击时,动画开始。

The stateful widget creates the controller, plays the animation, and builds the non-animating portion of the widget tree. The animation begins when a tap is detected anywhere in the screen.

Full code for basic_staggered_animation’s main.dart

无状态的 widget: StaggerAnimation

Stateless widget: StaggerAnimation

在无状态 widget 中,StaggerAnimation,the build() 函数实例化了一个 AnimatedBuilder—一个用于构建动画的通用 widget。 AnimatedBuilder 构建一个 widget 并使用 Tweens 的当前值配置它。这个例子创建一个名为 _buildAnimation() (实际更新 UI)的方法,并将其分配给其 builder 属性。AnimatedBuilder 监听来自动画控制器的通知,当值发生更改时,将 widget 树标记为 dirty。对于动画的每一个标记,值都会更新,导致调用 _buildAnimation()

In the stateless widget, StaggerAnimation, the build() function instantiates an AnimatedBuilder—a general purpose widget for building animations. The AnimatedBuilder builds a widget and configures it using the Tweens’ current values. The example creates a function named _buildAnimation() (which performs the actual UI updates), and assigns it to its builder property. AnimatedBuilder listens to notifications from the animation controller, marking the widget tree dirty as values change. For each tick of the animation, the values are updated, resulting in a call to _buildAnimation().

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({ Key key, this.controller }) :

    // Each animation defined here transforms its value during the subset
    // of the controller's duration defined by the animation's interval.
    // For example the opacity animation transforms its value during
    // the first 10% of the controller's duration.

    opacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.0, 0.100,
          curve: Curves.ease,
        ),
      ),
    ),

    // ... Other tween definitions ...

    super(key: key);

  final AnimationController controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius> borderRadius;
  final Animation<Color> color;

  // This function is called each time the controller "ticks" a new frame.
  // When it runs, all of the animation's values will have been
  // updated to reflect the controller's current value.
  Widget _buildAnimation(BuildContext context, Widget child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: height.value,
          decoration: BoxDecoration(
            color: color.value,
            border: Border.all(
              color: Colors.indigo[300],
              width: 3.0,
            ),
            borderRadius: borderRadius.value,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

有状态的 widget: StaggerDemo

Stateful widget: StaggerDemo

有状态的 widget, StaggerDemo,创建 AnimationController(控制所有动画的控制器),设定一个 2000 毫秒的周期。控制器播放一个动画,然后在 widget 树上创建一个无动画的部分。当在屏幕上检测到一个点击时,动画开始。动画向前运行,然后向后运行。

The stateful widget, StaggerDemo, creates the AnimationController (the one who rules them all), specifying a 2000 ms duration. It plays the animation, and builds the non-animating portion of the widget tree. The animation begins when a tap is detected in the screen. The animation runs forward, then backward.

class StaggerDemo extends StatefulWidget {
  @override
  _StaggerDemoState createState() => _StaggerDemoState();
}

class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this
    );
  }

  // ...Boilerplate...

  Future<void> _playAnimation() async {
    try {
      await _controller.forward().orCancel;
      await _controller.reverse().orCancel;
    } on TickerCanceled {
      // the animation got canceled, probably because it was disposed of
    }
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 10.0; // 1.0 is normal animation speed.
    return Scaffold(
      appBar: AppBar(
        title: const Text('Staggered Animation'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(
                color:  Colors.black.withOpacity(0.5),
              ),
            ),
            child: StaggerAnimation(
              controller: _controller.view
            ),
          ),
        ),
      ),
    );
  }
}

使用操作和快捷方式

目录

This page describes how to bind physical keyboard events to actions in the user interface. For instance, to define keyboard shortcuts in your application, this page is for you.

Overview

For a GUI application to do anything, it has to have actions: users want to tell the application to do something. Actions are often simple functions that directly perform the action (such as set a value or save a file). In a larger application, however, things are more complex: the code for invoking the action, and the code for the action itself might need to be in different places. Shortcuts (key bindings) might need definition at a level that knows nothing about the actions they invoke.

That’s where Flutter’s actions and shortcuts system comes in. It allows developers to define actions that fulfill intents bound to them. In this context, an intent is a generic action that the user wishes to perform, and an Intent class instance represents these user intents in Flutter. An Intent can be general purpose, fulfilled by different actions in different contexts. An Action can be a simple callback (as in the case of the CallbackAction) or something more complex that integrates with entire undo/redo architectures (for example) or other logic.

Using Shortcuts Diagram

Shortcuts are key bindings that activate by pressing a key or combination of keys. The key combinations reside in a table with their bound intent. When the Shortcuts widget invokes them, it sends their matching intent to the actions subsystem for fulfillment.

To illustrate the concepts in actions and shortcuts, this article creates a simple app that allows a user to select and copy text in a text field using both buttons and shortcuts.

Why separate Actions from Intents?

You might wonder: why not just map a key combination directly to an action? Why have intents at all? This is because it is useful to have a separation of concerns between where the key mapping definitions are (often at a high level), and where the action definitions are (often at a low level), and because it is important to be able to have a single key combination map to an intended operation in an app, and have it adapt automatically to whichever action fulfills that intended operation for the focused context.

For instance, Flutter has an ActivateIntent widget that maps each type of control to its corresponding version of an ActivateAction (and that executes the code that activates the control). This code often needs fairly private access to do its work. If the extra layer of indirection that Intents provide didn’t exist, it would be necessary to elevate the definition of the actions to where the defining instance of the Shortcuts widget could see them, causing the shortcuts to have more knowledge than necessary about which action to invoke, and to have access to or provide state that it wouldn’t necessarily have or need otherwise. This allows your code to separate the two concerns to be more independent.

Intents configure an action so that the same action can serve multiple uses. An example of this is DirectionalFocusIntent, which takes a direction to move the focus in, allowing the DirectionalFocusAction to know which direction to move the focus. Just be careful: don’t pass state in the Intent that applies to all invocations of an Action: that kind of state should be passed to the constructor of the Action itself, to keep the Intent from needing to know too much.

Why not use callbacks?

You also might wonder: why not just use a callback instead of an Action object? The main reason is that it’s useful for actions to decide whether they are enabled by implementing isEnabled. Also, it is often helpful if the key bindings, and the implementation of those bindings, are in different places.

If indeed all that is needed is a callback, without all the complexity (or flexibility) of Actions and Shortcuts, you can already use a Focus widget for this. For example, here’s the implementation of Flutter’s simple CallbackShortcuts widget (available on the dev branch) that takes a map of activators and executes callbacks for them:

class CallbackShortcuts extends StatelessWidget {
  const CallbackShortcuts({
    Key? key,
    required this.bindings,
    required this.child,
  }) : super(key: key);

  final Map<ShortcutActivator, VoidCallback> bindings;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        KeyEventResult result = KeyEventResult.ignored;
        // Activates all key bindings that match, returns handled if any handle it.
        for (final ShortcutActivator activator in bindings.keys) {
          if (activator.accepts(event, RawKeyboard.instance)) {
            bindings[activator]!.call();
            result = KeyEventResult.handled;
          }
        }
        return result;
      },
      child: child,
    );
  }
}

This may be all that is needed for some apps.

Shortcuts

As you’ll see below, actions are useful on their own, but the most common use case involves binding them to a keyboard shortcut. This is what the Shortcuts widget is for.

It is inserted into the widget hierarchy to define key combinations that represent the user’s intent when that key combination is pressed. To convert that intended purpose for the key combination into a concrete action, the Actions widget used to map the Intent to an Action. For instance, you can define a SelectAllIntent, and bind it to your own SelectAllAction or to your CanvasSelectAllAction, and from that one key binding, the system invokes either one, depending on which part of your application has focus. Let’s see how the key binding part works:

@override
Widget build(BuildContext context) {
  return Shortcuts(
    shortcuts: <LogicalKeySet, Intent>{
      LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
          SelectAllIntent(),
    },
    child: Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        SelectAllIntent: SelectAllAction(model),
      },
      child: Builder(
        builder: (BuildContext context) => TextButton(
          child: const Text('SELECT ALL'),
          onPressed: Actions.handler<SelectAllIntent>(
            context,
            SelectAllIntent(),
          ),
        ),
      ),
    ),
  );
}

The map given to a Shortcuts widget maps a LogicalKeySet (or a ShortcutActivator, see note below) to an Intent instance. The logical key set defines a set of one or more keys, and the intent indicates the intended purpose of the keypress. The Shortcuts widget looks up keypresses in the map, to find an Intent instance, which it gives to the action’s invoke() method.

The ShortcutManager

The shortcut manager, a longer-lived object than the Shortcuts widget, passes on key events when it receives them. It contains the logic for deciding how to handle the keys, the logic for walking up the tree to find other shortcut mappings, and maintains a map of key combinations to intents.

While the default behavior of the ShortcutManager is usually desirable, the Shortcuts widget takes a ShortcutManager that you can subclass to customize its functionality.

For example, if you wanted to log each key that a Shortcuts widget handled, you could make a LoggingShortcutManager:

class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}

Now, every time the Shortcuts widget handles a shortcut, it prints out the key event and relevant context.

Actions

Actions allow for the definition of operations that the application can perform by invoking them with an Intent. Actions can be enabled or disabled, and receive the intent instance that invoked them as an argument to allow configuration by the intent.

Defining actions

Actions, in their simplest form, are just subclasses of Action<Intent> with an invoke() method. Here’s a simple action that simply invokes a function on the provided model:

class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.model);

  final Model model;

  @override
  void invoke(covariant SelectAllIntent intent) => model.selectAll();
}

Or, if it’s too much of a bother to create a new class, use a CallbackAction:

CallbackAction(onInvoke: (Intent intent) => model.selectAll());

Once you have an action, you add it to your application using the Actions widget, which takes a map of Intent types to Actions:

@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: child,
  );
}

The Shortcuts widget uses the Focus widget’s context and Actions.invoke to find which action to invoke. If the Shortcuts widget doesn’t find a matching intent type in the first Actions widget encountered, it considers the next ancestor Actions widget, and so on, until it reaches the root of the widget tree, or finds a matching intent type and invokes the corresponding action.

Invoking Actions

The actions system has several ways to invoke actions. By far the most common way is through the use of a Shortcuts widget covered in the previous section, but there are other ways to interrogate the actions subsystem and invoke an action. It’s possible to invoke actions that are not bound to keys.

For instance, to find an action associated with an intent, you can use:

Action<SelectAllIntent>? selectAll =
    Actions.maybeFind<SelectAllIntent>(context);

This returns an Action associated with the SelectAllIntent type if one is available in the given context. If one isn’t available, it returns null. If an associated Action should always be available, then use find instead of maybeFind, which throws an exception when it doesn’t find a matching Intent type.

To invoke the action (if it exists), call:

Object? result;
if (selectAll != null) {
  result = Actions.of(context).invokeAction(selectAll, SelectAllIntent());
}

Combine that into one call with the following:

Object? result =
    Actions.maybeInvoke<SelectAllIntent>(context, SelectAllIntent());

Sometimes you want to invoke an action as a result of pressing a button or another control. Do this with the Actions.handler function, which creates a handler closure if the intent has a mapping to an enabled action, and returns null if it doesn’t, so that the button is disabled if there is no matching enabled action in the context:

@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: Builder(
      builder: (BuildContext context) => TextButton(
        child: const Text('SELECT ALL'),
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          SelectAllIntent(controller: controller),
        ),
      ),
    ),
  );
}

The Actions widget only invokes actions when isEnabled(Intent intent) returns true, allowing the action to decide if the dispatcher should consider it for invocation. If the action isn’t enabled, then the Actions widget gives another enabled action higher in the widget hierarchy (if it exists) a chance to execute.

The previous example uses a Builder because Actions.handler and Actions.invoke (for example) only finds actions in the provided context, and if the example passes the context given to the build function, the framework starts looking above the current widget. Using a Builder allows the framework to find the actions defined in the same build function.

You can invoke an action without needing a BuildContext, but since the Actions widget requires a context to find an enabled action to invoke, you need to provide one, either by creating your own Action instance, or by finding one in an appropriate context with Actions.find.

To invoke the action, pass the action to the invoke method on an ActionDispatcher, either one you created yourself, or one retrieved from an existing Actions widget using the Actions.of(context) method. Check whether the action is enabled before calling invoke. Of course, you can also just call invoke on the action itself, passing an Intent, but then you opt out of any services that an action dispatcher might provide (like logging, undo/redo, and so on).

Action dispatchers

Most of the time, you just want to invoke an action, have it do its thing, and forget about it. Sometimes, however, you might want to log the executed actions.

This is where replacing the default ActionDispatcher with a custom dispatcher comes in. You pass your ActionDispatcher to the Actions widget, and it invokes actions from any Actions widgets below that one that doesn’t set a dispatcher of its own.

The first thing Actions does when invoking an action is look up the ActionDispatcher and pass the action to it for invocation. If there is none, it creates a default ActionDispatcher that simply invokes the action.

If you want a log of all the actions invoked, however, you can create your own LoggingActionDispatcher to do the job:

class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);
  }
}

Then you pass that to your top-level Actions widget:

@override
Widget build(BuildContext context) {
  return Actions(
    dispatcher: LoggingActionDispatcher(),
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: Builder(
      builder: (BuildContext context) => TextButton(
        child: const Text('SELECT ALL'),
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          SelectAllIntent(),
        ),
      ),
    ),
  );
}

This logs every action as it executes, like so:

flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])

Putting it together

The combination of Actions and Shortcuts is powerful: you can define generic intents that map to specific actions at the widget level. Here’s a simple app that illustrates the concepts described above. The app creates a text field that also has “select all” and “copy to clipboard” buttons next to it. The buttons invoke actions to accomplish their work. All the invoked actions and shortcuts are logged.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
  const CopyableTextField({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<CopyableTextField> createState() => _CopyableTextFieldState();
}

class _CopyableTextFieldState extends State<CopyableTextField> {
  late TextEditingController controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        ClearIntent: ClearAction(controller),
        CopyIntent: CopyAction(controller),
        SelectAllIntent: SelectAllAction(controller),
      },
      child: Builder(builder: (BuildContext context) {
        return Scaffold(
          body: Center(
            child: Row(
              children: <Widget>[
                const Spacer(),
                Expanded(
                  child: TextField(controller: controller),
                ),
                IconButton(
                  icon: const Icon(Icons.copy),
                  onPressed:
                      Actions.handler<CopyIntent>(context, const CopyIntent()),
                ),
                IconButton(
                  icon: const Icon(Icons.select_all),
                  onPressed: Actions.handler<SelectAllIntent>(
                      context, const SelectAllIntent()),
                ),
                const Spacer(),
              ],
            ),
          ),
        );
      }),
    );
  }
}

/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}

/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);
  }
}

/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
  const ClearIntent();
}

/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
  ClearAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant ClearIntent intent) {
    controller.clear();
  }
}

/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
  const CopyIntent();
}

/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
  CopyAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant CopyIntent intent) {
    final String selectedString = controller.text.substring(
      controller.selection.baseOffset,
      controller.selection.extentOffset,
    );
    Clipboard.setData(ClipboardData(text: selectedString));
  }
}

/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
  const SelectAllIntent();
}

/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant SelectAllIntent intent) {
    controller.selection = controller.selection.copyWith(
      baseOffset: 0,
      extentOffset: controller.text.length,
      affinity: controller.selection.affinity,
    );
  }
}

/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String title = 'Shortcuts and Actions Demo';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Shortcuts(
        manager: LoggingShortcutManager(),
        shortcuts: <LogicalKeySet, Intent>{
          LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
              const CopyIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
              const SelectAllIntent(),
        },
        child: const CopyableTextField(title: title),
      ),
    );
  }
}

void main() => runApp(const MyApp());

Understanding Flutter's focus system

目录

This article explains how to control where keyboard input is directed. If you are implementing an application that uses a physical keyboard, such as most desktop and web applications, this page is for you. If your app won’t be used with a physical keyboard, you can skip this.

Overview

Flutter comes with a focus system that directs the keyboard input to a particular part of an application. In order to do this, users “focus” the input onto that part of an application by tapping or clicking the desired UI element. Once that happens, text entered with the keyboard flows to that part of the application until the focus moves to another part of the application. Focus can also be moved by pressing a particular keyboard shortcut, which is typically bound to the Tab key, so it is sometimes called “tab traversal”.

This page explores the APIs used to perform these operations on a Flutter application, and how the focus system works. We have noticed that there is some confusion among developers about how to define and use FocusNode objects. If that describes your experience, skip ahead to the best practices for creating FocusNode objects.

Focus use cases

Some examples of situations where you might need to know how to use the focus system:

Glossary

Below are terms, as Flutter uses them, for elements of the focus system. The various classes that implement some of these concepts are introduced below.

FocusNode and FocusScopeNode

The FocusNode and FocusScopeNode objects implement the mechanics of the focus system. They are long-lived objects (longer than widgets, similar to render objects) that hold the focus state and attributes so that they are persistent between builds of the widget tree. Together, they form the focus tree data structure.

They were originally intended to be developer-facing objects used to control some aspects of the focus system, but over time they have evolved to mostly implement details of the focus system. In order to prevent breaking existing applications, they still contain public interfaces for their attributes. But, in general, the thing for which they are most useful is to act as a relatively opaque handle, passed to a descendant widget in order to call requestFocus() on an ancestor widget, which requests that a descendant widget obtain focus. Setting of the other attributes is best managed by a Focus or FocusScope widget, unless you are not using them, or implementing your own version of them.

Best practices for creating FocusNode objects

Some dos and don’ts around using these objects include:

Unfocusing

There is an API for telling a node to “give up the focus”, named FocusNode.unfocus(). While it does remove focus from the node, it is important to realize that there really is no such thing as “unfocusing” all nodes. If a node is unfocused, then it must pass the focus somewhere else, since there is always a primary focus. The node that receives the focus when a node calls unfocus() is either the nearest FocusScopeNode, or a previously focused node in that scope, depending upon the disposition argument given to unfocus(). If you would like more control over where the focus goes when you remove it from a node, explicitly focus another node instead of calling unfocus(), or use the focus traversal mechanism to find another node with the focusInDirection, nextFocus, or previousFocus methods on FocusNode.

When calling unfocus(), the disposition argument allows two modes for unfocusing: UnfocusDisposition.scope and UnfocusDisposition.previouslyFocusedChild. The default is scope, which gives the focus to the nearest parent focus scope. This means that if the focus is thereafter moved to the next node with FocusNode.nextFocus, it starts with the “first” focusable item in the scope.

The previouslyFocusedChild disposition will search the scope to find the previously focused child and request focus on it. If there is no previously focused child, it is equivalent to scope.

Focus widget

The Focus widget owns and manages a focus node, and is the workhorse of the focus system. It manages the attaching and detaching of the focus node it owns from the focus tree, manages the attributes and callbacks of the focus node, and has static functions to enable discovery of focus nodes attached to the widget tree.

In its simplest form, wrapping the Focus widget around a widget subtree allows that widget subtree to obtain focus as part of the focus traversal process, or whenever requestFocus is called on the FocusNode passed to it. When combined with a gesture detector that calls requestFocus, it can receive focus when tapped or clicked.

You might pass a FocusNode object to the Focus widget to manage, but if you don’t, it creates its own. The main reason to create your own FocusNode is to be able to call requestFocus() on the node to control the focus from a parent widget. Most of the other functionality of a FocusNode is best accessed by changing the attributes of the Focus widget itself.

The Focus widget is used in most of Flutter’s own controls to implement their focus functionality.

Here is an example showing how to use the Focus widget to make a custom control focusable. It creates a container with text that reacts to receiving the focus.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static const String _title = 'Focus Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[MyCustomWidget(), MyCustomWidget()],
        ),
      ),
    );
  }
}

class MyCustomWidget extends StatefulWidget {
  const MyCustomWidget({Key? key}) : super(key: key);

  @override
  State<MyCustomWidget> createState() => _MyCustomWidgetState();
}

class _MyCustomWidgetState extends State<MyCustomWidget> {
  Color _color = Colors.white;
  String _label = 'Unfocused';

  @override
  Widget build(BuildContext context) {
    return Focus(
      onFocusChange: (bool focused) {
        setState(() {
          _color = focused ? Colors.black26 : Colors.white;
          _label = focused ? 'Focused' : 'Unfocused';
        });
      },
      child: Center(
        child: Container(
          width: 300,
          height: 50,
          alignment: Alignment.center,
          color: _color,
          child: Text(_label),
        ),
      ),
    );
  }
}

Key events

If you wish to listen for key events in a subtree, set the onKey attribute of the Focus widget to be a handler that either just listens to the key, or handles the key and stops its propagation to other widgets.

Key events start at the focus node with primary focus. If that node doesn’t return KeyEventResult.handled from its onKey handler, then its parent focus node is given the event. If the parent doesn’t handle it, it goes to its parent, and so on, until it reaches the root of the focus tree. If the event reaches the root of the focus tree without being handled, then it is returned to the platform to give to the next native control in the application (in case the Flutter UI is part of a larger native application UI). Events that are handled are not propagated to other Flutter widgets, and they are also not propagated to native widgets.

Here’s an example of a Focus widget that absorbs every key that its subtree doesn’t handle, without being able to be the primary focus:

@override
Widget build(BuildContext context) {
  return Focus(
    onKey: (FocusNode node, RawKeyEvent event) => KeyEventResult.handled,
    canRequestFocus: false,
    child: child,
  );
}

Focus key events are processed before text entry events, so handling a key event when the focus widget surrounds a text field prevents that key from being entered into the text field.

Here’s an example of a widget that won’t allow the letter “a” to be typed into the text field:

@override
Widget build(BuildContext context) {
  return Focus(
    onKey: (FocusNode node, RawKeyEvent event) {
      return (event.logicalKey == LogicalKeyboardKey.keyA)
          ? KeyEventResult.handled
          : KeyEventResult.ignored;
    },
    child: TextField(),
  );
}

If the intent is input validation, this example’s functionality would probably be better implemented using a TextInputFormatter, but the technique can still be useful: the Shortcuts widget uses this method to handle shortcuts before they become text input, for instance.

Controlling what gets focus

One of the main aspects of focus is controlling what can receive focus and how. The attributes canRequestFocus, skipTraversal, and descendantsAreFocusable control how this node and its descendants participate in the focus process.

If the skipTraversal attribute true, then this focus node doesn’t participate in focus traversal. It is still focusable if requestFocus is called on its focus node, but is otherwise skipped when the focus traversal system is looking for the next thing to focus on.

The canRequestFocus attribute, unsurprisingly, controls whether or not the focus node that this Focus widget manages can be used to request focus. If this attribute is false, then calling requestFocus on the node has no effect. It also implies that this node is skipped for focus traversal, since it can’t request focus.

The descendantsAreFocusable attribute controls whether the descendants of this node can receive focus, but still allows this node to receive focus. This attribute can be used to turn off focusability for an entire widget subtree. This is how the ExcludeFocus widget works: it’s just a Focus widget with this attribute set.

Autofocus

Setting the autofocus attribute of a Focus widget tells the widget to request the focus the first time the focus scope it belongs to is focused. If more than one widget has autofocus set, then it is arbitrary which one receives the focus, so try to only set it on one widget per focus scope.

The autofocus attribute only takes effect if there isn’t already a focus in the scope that the node belongs to.

Setting the autofocus attribute on two nodes that belong to different focus scopes is well defined: each one becomes the focused widget when their corresponding scopes are focused.

Change notifications

The Focus.onFocusChanged callback can be used to get notifications that the focus state for a particular node has changed. It notifies if the node is added to or removed from the focus chain, which means it gets notifications even if it isn’t the primary focus. If you only want to know if you have received the primary focus, check and see if hasPrimaryFocus is true on the focus node.

Obtaining the FocusNode

Sometimes, it is useful to obtain the focus node of a Focus widget to interrogate its attributes.

To access the focus node from an ancestor of the Focus widget, create and pass in a FocusNode as the Focus widget’s focusNode attribute. Because it needs to be disposed of, the focus node you pass needs to be owned by a stateful widget, so don’t just create one each time it is built.

If you need access to the focus node from the descendant of a Focus widget, you can call Focus.of(context) to obtain the focus node of the nearest Focus widget to the given context. If you need to obtain the FocusNode of a Focus widget within the same build function, use a Builder to make sure you have the correct context. This is shown in the following example:

@override
Widget build(BuildContext context) {
  return Focus(
    child: Builder(
      builder: (BuildContext context) {
        final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
        print('Building with primary focus: $hasPrimary');
        return const SizedBox(width: 100, height: 100);
      },
    ),
  );
}

Timing

One of the details of the focus system is that when focus is requested, it only takes effect after the current build phase completes. This means that focus changes are always delayed by one frame, because changing focus can cause arbitrary parts of the widget tree to rebuild, including ancestors of the widget currently requesting focus. Because descendants cannot dirty their ancestors, it has to happen between frames, so that any needed changes can happen on the next frame.

FocusScope widget

The FocusScope widget is a special version of the Focus widget that manages a FocusScopeNode instead of a FocusNode. The FocusScopeNode is a special node in the focus tree that serves as a grouping mechanism for the focus nodes in a subtree. Focus traversal stays within a focus scope unless a node outside of the scope is explicitly focused.

The focus scope also keeps track of the current focus and history of the nodes focused within its subtree. That way, if a node releases focus or is removed when it had focus, the focus can be returned to the node that had focus previously.

Focus scopes also serve as a place to return focus to if none of the descendants have focus. This allows the focus traversal code to have a starting context for finding the next (or first) focusable control to move to.

If you focus a focus scope node, it first attempts to focus the current, or most recently focused node in its subtree, or the node in its subtree that requested autofocus (if any). If there is no such node, it receives the focus itself.

FocusableActionDetector widget

The FocusableActionDetector is a widget that combines the functionality of Actions, Shortcuts, MouseRegion and a Focus widget to create a detector that defines actions and key bindings, and provides callbacks for handling focus and hover highlights. It is what Flutter controls use to implement all of these aspects of the controls. It is just implemented using the constituent widgets, so if you don’t need all of its functionality, you can just use the ones you need, but it is a convenient way to build these behaviors into your custom controls.

Controlling focus traversal

Once an application has the ability to focus, the next thing many apps want to do is to allow the user to control the focus using the keyboard or another input device. The most common example of this is “tab traversal” where the user presses the Tab key to go to the “next” control. Controlling what “next” means is the subject of this section. This kind of traversal is provided by Flutter by default.

In a simple grid layout, it’s fairly easy to decide which control is next. If you’re not at the end of the row, then it’s the one to the right (or left for right-to-left locales). If you are at the end of a row, it’s the first control in the next row. Unfortunately, applications are rarely laid out in grids, so more guidance is often needed.

The default algorithm in Flutter (ReadingOrderTraversalPolicy) for focus traversal is pretty good: It gives the right answer for most applications. However, there are always pathological cases, or cases where the context or design requires a different order than the one the default ordering algorithm arrives at. For those cases, there are other mechanisms for achieving the desired order.

FocusTraversalGroup widget

The FocusTraversalGroup widget should be placed in the tree around widget subtrees that should be fully traversed before moving on to another widget or group of widgets. Just grouping widgets into related groups is often enough to resolve many tab traversal ordering problems. If not, the group can also be given a FocusTraversalPolicy to determine the ordering within the group.

The default ReadingOrderTraversalPolicy is usually sufficient, but in cases where more control over ordering is needed, an OrderedTraversalPolicy can be used. The order argument of the FocusTraversalOrder widget wrapped around the focusable components determines the order. The order can be any subclass of FocusOrder, but NumericFocusOrder and LexicalFocusOrder are provided.

If none of the provided focus traversal policies are sufficient for your application, you could also write your own policy and use it to determine any custom ordering you want.

Here’s an example of how to use the FocusTraversalOrder widget to traverse a row of buttons in the order TWO, ONE, THREE using NumericFocusOrder.

class OrderedButtonRow extends StatelessWidget {
  const OrderedButtonRow({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FocusTraversalGroup(
      policy: OrderedTraversalPolicy(),
      child: Row(
        children: <Widget>[
          const Spacer(),
          FocusTraversalOrder(
            order: NumericFocusOrder(2.0),
            child: TextButton(
              child: const Text('ONE'),
              onPressed: () {},
            ),
          ),
          const Spacer(),
          FocusTraversalOrder(
            order: NumericFocusOrder(1.0),
            child: TextButton(
              child: const Text('TWO'),
              onPressed: () {},
            ),
          ),
          const Spacer(),
          FocusTraversalOrder(
            order: NumericFocusOrder(3.0),
            child: TextButton(
              child: const Text('THREE'),
              onPressed: () {},
            ),
          ),
          const Spacer(),
        ],
      ),
    );
  }
}

FocusTraversalPolicy

The FocusTraversalPolicy is the object that determines which widget is next, given a request and the current focus node. The requests (member functions) are things like findFirstFocus, findLastFocus, next, previous, and inDirection.

FocusTraversalPolicy is the abstract base class for concrete policies, like ReadingOrderTraversalPolicy, OrderedTraversalPolicy and the DirectionalFocusTraversalPolicyMixin classes.

In order to use a FocusTraversalPolicy, you give one to a FocusTraversalGroup, which determines the widget subtree in which the policy will be effective. The member functions of the class are rarely called directly: they are meant to be used by the focus system.

The focus manager

The FocusManager maintains the current primary focus for the system. It only has a few pieces of API that are useful to users of the focus system. One is the FocusManager.instance.primaryFocus property, which contains the currently focused focus node and is also accessible from the global primaryFocus field.

Other useful properties are FocusManager.instance.highlightMode and FocusManager.instance.highlightStrategy. These are used by widgets that need to switch between a “touch” mode and a “traditional” (mouse and keyboard) mode for their focus highlights. When a user is using touch to navigate, the focus highlight is usually hidden, and when they switch to a mouse or keyboard, the focus highlight needs to be shown again so they know what is focused. The hightlightStrategy tells the focus manager how to interpret changes in the usage mode of the device: it can either automatically switch between the two based on the most recent input events, or it can be locked in touch or traditional modes. The provided widgets in Flutter already know how to use this information, so you only need it if you’re writing your own controls from scratch. You can use addHighlightModeListener callback to listen for changes in the highlight mode.

点击、拖动和其他手势

目录

这个章节将会讲解如何监听和响应 Flutter 的手势操作 gestures。典型的手势操作包括点击、拖动和缩放。

This document explains how to listen for, and respond to, gestures in Flutter. Examples of gestures include taps, drags, and scaling.

Flutter 中的手势有两个不同的层次:第一层是原始的指针指向事件,描述了屏幕上由触摸板、鼠标、指示笔等触发的指针的位置和移动。第二层包含 gestures,描述了由上述一个或多个指针移动组成的具有特殊语义的操作。

The gesture system in Flutter has two separate layers. The first layer has raw pointer events that describe the location and movement of pointers (for example, touches, mice, and styli) across the screen. The second layer has gestures that describe semantic actions that consist of one or more pointer movements.

指针

Pointers

Pointer 代表的是人机界面交互的原始数据。一共有四种指针事件:

Pointers represent raw data about the user’s interaction with the device’s screen. There are four types of pointer events:

PointerDownEvent
指针在特定位置与屏幕接触

PointerDownEvent
The pointer has contacted the screen at a particular location.

PointerMoveEvent
指针从屏幕的一个位置移动到另外一个位置

PointerMoveEvent
The pointer has moved from one location on the screen to another.

PointerUpEvent
指针与屏幕停止接触

PointerUpEvent
The pointer has stopped contacting the screen.

PointerCancelEvent
指针的输入已经不再指向此应用

PointerCancelEvent
Input from this pointer is no longer directed towards this app.

在指针下落事件中,框架做了一个 hit test 的操作确定与屏幕发生接触的位置上有哪些组件以及分发给最内部的组件去响应。事件会沿着组件树从这个最内部的组件向组件树的根部冒泡分发。并且不存在用于取消或停止指针事件进行进一步分发的机制。

On pointer down, the framework does a hit test on your app to determine which widget exists at the location where the pointer contacted the screen. The pointer down event (and subsequent events for that pointer) are then dispatched to the innermost widget found by the hit test. From there, the events bubble up the tree and are dispatched to all the widgets on the path from the innermost widget to the root of the tree. There is no mechanism for canceling or stopping pointer events from being dispatched further.

使用 Listener 可以在组件层直接监听指针事件。然而,一般情况下,请考虑使用下面的 gestures 替代。

To listen to pointer events directly from the widgets layer, use a Listener widget. However, generally, consider using gestures (as discussed below) instead.

手势

Gestures

Gesture 代表的是语义操作(比如点击、拖动、缩放)。通常由一系列单独的指针事件组成,甚至是一系列单独的指针组成。 Gesture 可以分发多种事件,对应着手势的生命周期(比如开始拖动、拖动更新、结束拖动)。

Gestures represent semantic actions (for example, tap, drag, and scale) that are recognized from multiple individual pointer events, potentially even multiple individual pointers. Gestures can dispatch multiple events, corresponding to the lifecycle of the gesture (for example, drag start, drag update, and drag end):

点击

Tap

onTapDown
指针在发生接触的屏幕的特定位置可能引发点击事件。

onTapDown
A pointer that might cause a tap has contacted the screen at a particular location.

onTapUp
指针使在屏幕的特定位置触发的点击事件停止。

onTapUp
A pointer that will trigger a tap has stopped contacting the screen at a particular location.

onTap
点击事件已经发生。

onTap
A tap has occurred.

onTapCancel
指针已经触发了 onTapDown,但是最终不会形成一个点击事件。

onTapCancel
The pointer that previously triggered the onTapDown will not end up causing a tap.

双击

Double tap

onDoubleTap
用户在屏幕的相同位置上快速点击了两次。

onDoubleTap
The user has tapped the screen at the same location twice in quick succession.

长按

Long press

onLongPress
指针在屏幕的相同位置上保持接触持续一长段时间。

onLongPress
A pointer has remained in contact with the screen at the same location for a long period of time.

纵向拖动

Vertical drag

onVerticalDragStart
指针和屏幕产生接触并可能开始纵向移动。

onVerticalDragStart
A pointer has contacted the screen and might begin to move vertically.

onVerticalDragUpdate
指针和屏幕产生接触,在纵向上发生移动并保持移动。

onVerticalDragUpdate
A pointer that is in contact with the screen and moving vertically has moved in the vertical direction.

onVerticalDragEnd
指针先前和屏幕产生了接触,以特定速度纵向移动,并且此后不会在屏幕接触上发生纵向移动。

onVerticalDragEnd
A pointer that was previously in contact with the screen and moving vertically is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.

横向拖动

Horizontal drag

onHorizontalDragStart
指针和屏幕产生接触并可能开始横向移动。

onHorizontalDragStart
A pointer has contacted the screen and might begin to move horizontally.

onHorizontalDragUpdate
指针和屏幕产生接触,在横向上发生移动并保持移动。

onHorizontalDragUpdate
A pointer that is in contact with the screen and moving horizontally has moved in the horizontal direction.

onHorizontalDragEnd
指针先前和屏幕产生了接触,以特定速度横向移动,并且此后不会在屏幕接触上发生横向移动。

onHorizontalDragEnd
A pointer that was previously in contact with the screen and moving horizontally is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.

移动

Pan

onPanStart
指针和屏幕产生接触并可能开始横向移动或者纵向移动。如果设置了 onHorizontalDragStart 或者 onVerticalDragStart,该回调方法会引发崩溃。

onPanStart
A pointer has contacted the screen and might begin to move horizontally or vertically. This callback causes a crash if onHorizontalDragStart or onVerticalDragStart is set.

onPanUpdate
指针和屏幕产生接触,在横向或者纵向上发生移动并保持移动。如果设置了 onHorizontalDragUpdate 或者 onVerticalDragUpdate,该回调方法会引发崩溃。

onPanUpdate
A pointer that is in contact with the screen and is moving in the vertical or horizontal direction. This callback causes a crash if onHorizontalDragUpdate or onVerticalDragUpdate is set.

onPanEnd
指针先前和屏幕产生了接触,并且以特定速度移动,此后不再在屏幕接触上发生移动。如果设置了 onHorizontalDragEnd 或者 onVerticalDragEnd,该回调方法会引发崩溃。

onPanEnd
A pointer that was previously in contact with screen is no longer in contact with the screen and is moving at a specific velocity when it stopped contacting the screen. This callback causes a crash if onHorizontalDragEnd or onVerticalDragEnd is set.

为 widgets 添加手势检测

Adding gesture detection to widgets

从组件层监听手势,需要用到 GestureDetector

To listen to gestures from the widgets layer, use a GestureDetector.

如果使用 Material 风格的组件,其中的许多组件都能够支持响应点击或者手势事件。比如 IconButtonTextButton 响应了按压事件(点击事件), ListView 响应了滚动事件。如果使用了上述组件,你也可以使用 InkWell 来实现点击的“水波纹”效果。

If you’re using Material Components, many of those widgets already respond to taps or gestures. For example, IconButton and TextButton respond to presses (taps), and ListView responds to swipes to trigger scrolling. If you are not using those widgets, but you want the “ink splash” effect on a tap, you can use InkWell.

手势消歧处理

Gesture disambiguation

在屏幕的指定位置上,可能有多个手势捕捉器。所有的手势捕捉器监听了指针输入流事件并判断出特定手势。 GestureDetector widget 能够基于手势的回调是否非空决定是否应该尝试去识别该手势。

At a given location on screen, there might be multiple gesture detectors. All of these gesture detectors listen to the stream of pointer events as they flow past and attempt to recognize specific gestures. The GestureDetector widget decides which gestures to attempt to recognize based on which of its callbacks are non-null.

当屏幕上的指定指针有多个手势识别器时,框架会通过给每个手势识别器加入 gesture arena 来处理手势消歧。 gesture arena,也称作手势竞技场,会利用下述规则确定哪个手势在竞争中胜出:

When there is more than one gesture recognizer for a given pointer on the screen, the framework disambiguates which gesture the user intends by having each recognizer join the gesture arena. The gesture arena determines which gesture wins using the following rules:

比如,当纵向拖动和横向拖动需要处理消歧,当指针下落事件发生时,纵向和横向识别器都会进入竞技场,观测指针移动事件。如果用户在横向上移动超过了特定像素,横向识别器会宣告胜利,手势也会被当作横向拖动处理。同样的,如果用户在纵向上移动超过了特定的像素,纵向识别器会宣告胜利。

For example, when disambiguating horizontal and vertical dragging, both recognizers enter the arena when they receive the pointer down event. The recognizers observe the pointer move events. If the user moves the pointer more than a certain number of logical pixels horizontally, the horizontal recognizer declares victory and the gesture is interpreted as a horizontal drag. Similarly, if the user moves more than a certain number of logical pixels vertically, the vertical recognizer declares victory.

手势竞技场在仅有一个横向(或者纵向)拖动识别器的时候是非常高效的。在此示例中,竞技场中只有一个横向识别器,横向拖动就能够被立即识别到,这意味着横向移动从第一个像素开始就能够被立即处理成横向拖动手势,而并不需要等待进一步的手势消歧处理。

The gesture arena is beneficial when there is only a horizontal (or vertical) drag recognizer. In that case, there is only one recognizer in the arena and the horizontal drag is recognized immediately, which means the first pixel of horizontal movement can be treated as a drag and the user won’t need to wait for further gesture disambiguation.

使用 sliver 实现出色的滑动效果

A sliver is a portion of a scrollable area that you can define to behave in a special way. You can use slivers to achieve custom scrolling effects, such as elastic scrolling.

For a free, instructor-led video workshop that also uses DartPad, check out the following video about using slivers:

Resources

For more information on implementing fancy scrolling effects in Flutter, see the following resources:

Slivers, Demystified

A free article on Medium that explains how to implement custom scrolling using the sliver classes.

SliverAppBar

A one-minute Widget-of-the-week video that gives an overview of the SliverAppBar widget.

SliverList and SliverGrid

A one-minute Widget-of-the-week video that gives an overview of the SliverList and SliverGrid widgets.

Slivers explained - Making dynamic layouts

A 50-minute episode of The Boring Show where Ian Hickson, Flutter’s Tech Lead, and Filip Hracek discuss the power of slivers.

API docs

Here some links to relevant API docs:

向应用添加闪屏页

目录

Add Splash Screen Header

闪屏页(也称为启动页)是你的应用在启动时给用户的第一印象。它们就像是你的应用的基础,同时允许你在它展示的时间里,加载你的引擎和初始化你的应用。本指南将展示如何在 Flutter 编写的移动应用中恰当地使用闪屏页。

Splash screens (also known as launch screens) provide a simple initial experience while your mobile app loads. They set the stage for your application, while allowing time for the app engine to load and your app to initialize. This guide teaches you how to use splash screens appropriately on iOS and Android.

iOS 启动页

iOS launch screen

所有应用在交付到 Apple 应用商店之前 必须使用 Xcode storyboard 以提供应用启动页面。

All apps submitted to the Apple App Store must use an Xcode storyboard to provide the app’s launch screen.

默认的 Flutter 模板包括一个名为 LaunchScreen.storyboard 的 Xcode storyboard,可以根据您的选择进行定制你自己的资源。默认情况下,storyboard 将显示空白图像,但你可以修改它。在项目根目录下执行 open ios/Runner.xcworkspace 打开 Flutter 应用程序的 Xcode 项目。然后从项目导航器中选择 Runner/Assets.xcassets,并将所需图像拖拽至 LaunchImage 图像集中。

The default Flutter template includes an Xcode storyboard named LaunchScreen.storyboard that can be customized as you see fit with your own assets. By default, the storyboard displays a blank image, but you can change this. To do so, open the Flutter app’s Xcode project by typing open ios/Runner.xcworkspace from the root of your app directory. Then select Runner/Assets.xcassets from the Project Navigator and drop in the desired images to the LaunchImage image set.

Apple 在 人机接口指南 部分中为发布启动页提供了详细的指南。

Apple provides detailed guidance for launch screens as part of the Human Interface Guidelines.

Android 启动页

Android launch screen

在 Android 中,你有两个可以分开控制的页面:在 Android 应用初始化时的 启动页,以及在 Flutter 初始化时的 闪屏页

In Android, there are two separate screens that you can control: a launch screen shown while your Android app initializes, and a splash screen that displays while the Flutter experience initializes.

应用初始化

Initializing the app

所有 Android 应用在操作系统准备应用进程时都需要一定的初始化时间。因此 Android 提供了 启动界面 的概念,在应用初始化的时候显示 Drawable

Every Android app requires initialization time while the operating system sets up the app’s process. Android provides the concept of a launch screen to display a Drawable while the app is initializing.

默认的 Flutter 项目模板定义了启动主题和启动背景。你可以在 styles.xml 中自定义一个主题,将一个 Drawable 配置给该主题的 windowBackground,它将作为启动页被展示。

The default Flutter project template includes a definition of a launch theme and a launch background. You can customize this by editing styles.xml, where you can define a theme whose windowBackground is set to the Drawable that should be displayed as the launch screen.

<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
    <item name="android:windowBackground">@drawable/launch_background</item>
</style>

定义一个普通主题

Define a normal theme

此外,在 styles.xml 中定义一个 普通主题,当启动页消失后,它会应用在 FlutterActivity 上。普通主题的背景仅仅展示非常短暂的时间,例如,当启动页消失后、设备方向改变或者 Activity 恢复期间。因此建议普通主题的背景颜色使用与 Flutter UI 主要背景颜色相似的纯色。

In addition, style.xml defines a normal theme to be applied to FlutterActivity after the launch screen is gone. The normal theme background only shows for a very brief moment after the splash screen disappears, and during orientation change and Activity restoration. Therefore, it’s recommended that the normal theme use a solid background color that looks similar to the primary background color of the Flutter UI.

<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
    <item name="android:windowBackground">@drawable/normal_background</item>
</style>

在 AndroidManifest.xml 中配置 FlutterActivity

Set up the FlutterActivity in AndroidManifest.xml

AndroidManifest.xml 中,将 FlutterActivitytheme 设置为启动主题,将元数据元素添加到所需的 FlutterActivity,以知会 Flutter 在适当的时机从启动主题切换到普通主题。

In AndroidManifest.xml, set the theme of FlutterActivity to the launch theme. Then, add a metadata element to the desired FlutterActivity to instruct Flutter to switch from the launch theme to the normal theme at the appropriate time.

<activity
    android:name=".MyActivity"
    android:theme="@style/LaunchTheme"
    // ...
    >
    <meta-data
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/NormalTheme"
        />
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

如此一来,Android 应用程序就会在在初始化时展示对应的启动页面。

The Android app now displays the desired launch screen while the app initializes.

Android S

Android 12 (S)

请先查看 Android 闪屏页面 了解如何在 Android S 上配置闪屏页。

See Android Splash Screens first on how to configure your splash screen on Android S.

确保 io.flutter.embedding.android.SplashScreenDrawable 未在 manifest 中设置,且 provideSplashScreen 也没有具体实现,这些 API 已被废弃。如此一来 Android 的闪屏页可以在应用启动时平滑过渡到 Flutter。

Make sure neither io.flutter.embedding.android.SplashScreenDrawable is set in your manifest, nor is provideSplashScreen implemented, as these APIs are deprecated. Doing so will cause the Android splash screen to fade smoothly into the Flutter when the app is launched.

某些应用可能希望在 Flutter 中继续显示 Android 闪屏页的最后一帧。例如,保持一帧的展示,同时 Dart 继续加载其他内容。想达到这样的效果,以下 API 可能有帮助:

Some apps may want to continue showing the last frame of the Android splash screen in Flutter. For example, this preserves the illusion of a single frame while additional loading continues in Dart. To achieve this, the following Android APIs may be helpful:

import android.os.Build;
import android.os.Bundle;
import android.window.SplashScreenView;
import androidx.core.view.WindowCompat;
import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends FlutterActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Aligns the Flutter view vertically with the window.
        WindowCompat.setDecorFitsSystemWindows(getWindow(), false);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // Disable the Android splash screen fade out animation to avoid
            // a flicker before the similar frame is drawn in Flutter.
            getSplashScreen()
                .setOnExitAnimationListener(
                    (SplashScreenView splashScreenView) -> {
                        splashScreenView.remove();
                    });
        }

        super.onCreate(savedInstanceState);
    }
}
import android.os.Build
import android.os.Bundle
import androidx.core.view.WindowCompat
import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // Aligns the Flutter view vertically with the window.
    WindowCompat.setDecorFitsSystemWindows(getWindow(), false)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
      // Disable the Android splash screen fade out animation to avoid
      // a flicker before the similar frame is drawn in Flutter.
      splashScreen.setOnExitAnimationListener { splashScreenView -> splashScreenView.remove() }
    }

    super.onCreate(savedInstanceState)
  }
}

然后你可以重新实现 Flutter 的第一帧,将元素摆放在与 Android 闪屏页相同的位置。

Then, you can reimplement the first frame in Flutter that shows elements of your Android splash screen in the same positions on screen.

迁移 Manifest 和 Activity 中自定义的闪屏

Migrating from Manifest / Activity defined custom splash screens

在 Flutter 2.5 之前,Flutter 的 Android 应用要么是在 manifest 中设置 io.flutter.embedding.android.SplashScreenDrawable,要么是在 Flutter Activity 中实现 provideSplashScreen。这会导致在 Android 的启动页和 Flutter 的第一帧绘制之间,有短暂的空隙。这样的处理方法已被废弃,Flutter 现在会将 Android 的启动页保持到 Flutter 的第一帧渲染完成。开发者们可以直接删除这些 API 的使用。

Previously, Android Flutter apps would either set io.flutter.embedding.android.SplashScreenDrawable in their application manifest, or implement provideSplashScreen within their Flutter Activity. This would be shown momentarily in between the time after the Android launch screen is shown and when Flutter has drawn the first frame. This is no longer needed and is deprecated – Flutter now automatically keeps the Android launch screen displayed until Flutter has drawn the first frame. Developers should instead remove usage of these APIs.

核心 Widget 目录

借助 Flutter 上关于视觉、结构、平台和交互的 widgets,我们可以快速创建出色的应用程序。除了能够按照如下类别浏览 widgets,你还可以在 Flutter Widget 目录 中查看所有的 widgets。

Create beautiful apps faster with Flutter’s collection of visual, structural, platform, and interactive widgets. In addition to browsing widgets by category, you can also see all the widgets in the widget index.

Accessibility

Make your app accessible.

Animation and Motion

Bring animations to your app.

Assets, Images, and Icons

Manage assets, display images, and show icons.

Async

Async patterns to your Flutter application.

Basics

Widgets you absolutely need to know before building your first Flutter app.

Cupertino (iOS-style widgets)

Beautiful and high-fidelity widgets for current iOS design language.

Input

Take user input in addition to input widgets in Material Components and Cupertino.

Interaction Models

Respond to touch events and route users to different views.

Layout

Arrange other widgets columns, rows, grids, and many other layouts.

Material Components

Visual, behavioral, and motion-rich widgets implementing the Material Design guidelines.

Painting and effects

These widgets apply visual effects to the children without changing their layout, size, or position.

Scrolling

Scroll multiple widgets as children of the parent.

Styling

Manage the theme of your app, makes your app responsive to screen sizes, or add padding.

Text

Display and style text.

状态 (State) 管理介绍

如果你早已熟悉响应式 App 中的状态管理,你可以跳过这个部分,不过这里也有一些关于 状态 (State) 管理参考 的信息供你查阅。

If you are already familiar with state management in reactive apps, you can skip this section, though you might want to review the list of different approaches.

A short animated gif that shows the workings of a simple declarative state management system. This is explained in full in one of the following pages. Here it's just a decoration.

当你使用 Futter 进行开发时,有时会需要在 app 的不同界面中共享应用程序的状态,在这里你可以找到许多有用的方案以及一些可以深思的问题。

As you explore Flutter, there comes a time when you need to share application state between screens, across your app. There are many approaches you can take, and many questions to think about.

在接下来的文档里,你将会学习一些基础的状态管理知识。

In the following pages, you will learn the basics of dealing with state in Flutter apps.

状态管理中的声明式编程思维

如果你是从命令式框架(例如 Android SDK 或者 iOS UIKit)转到 Flutter 应用,那么,你需要开始从一个新的角度来考虑 app 开发了。

If you’re coming to Flutter from an imperative framework (such as Android SDK or iOS UIKit), you need to start thinking about app development from a new perspective.

因此,很多在命令式框架下的假设可能并不适用于 Flutter。例如,在 Flutter 应用中这是可行的,重新构建你的部分界面,而不是直接去修改它。如果有需要的话,Flutter 甚至可以在每一帧上都很快做到这点。

Many assumptions that you might have don’t apply to Flutter. For example, in Flutter it’s okay to rebuild parts of your UI from scratch instead of modifying it. Flutter is fast enough to do that, even on every frame if needed.

Flutter 应用是 声明式 的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态。

Flutter is declarative. This means that Flutter builds its user interface to reflect the current state of your app:

A mathematical formula of UI = f(state). 'UI' is the layout on the screen. 'f' is your build methods. 'state' is the application state.

当你的 Flutter 应用的状态发生改变时(例如,用户在设置界面中点击了一个开关选项)你改变了状态,这将会触发用户界面的重绘。去改变用户界面本身是没有必要的(例如 widget.setText )—你改变了状态,那么用户界面将重新构建。

When the state of your app changes (for example, the user flips a switch in the settings screen), you change the state, and that triggers a redraw of the user interface. There is no imperative changing of the UI itself (like widget.setText)—you change the state, and the UI rebuilds from scratch.

声明式 UI 介绍 中你可以阅读更多有关声明式编程思维的信息。

Read more about the declarative approach to UI programming in the get started guide.

声明式的编程风格有许多好处。值得注意的是,用户界面任何状态的改变都只有一种编码途径。一旦给定任意状态,你就描述了用户界面应该长什么样,并且它就是这样。

The declarative style of UI programming has many benefits. Remarkably, there is only one code path for any state of the UI. You describe what the UI should look like for any given state, once—and that is it.

刚开始的时候,这种编码风格可能看起来不像命令式的那么直观。这也是本章为什么出现在这的原因。

At first, this style of programming might not seem as intuitive as the imperative style. This is why this section is here. Read on.

短时 (ephemeral) 和应用 (app) 状态的区别

目录

本文将介绍应用 (app) 状态、短时 (ephemeral) 状态,以及在一个 Flutter 应用中你可以如何应用这两种状态。

This doc introduces app state, ephemeral state, and how you might manage each in a Flutter app.

广义上来讲,一个应用的状态就是当这个应用运行时存在于内存中的所有内容。这包括了应用中用到的资源,所有 Flutter 框架中有关用户界面、动画状态、纹理、字体以及其他等等的变量。这个对于状态广义的定义是有效的,但是它对于构建一个应用来说并不是很有用。

In the broadest possible sense, the state of an app is everything that exists in memory when the app is running. This includes the app’s assets, all the variables that the Flutter framework keeps about the UI, animation state, textures, fonts, and so on. While this broadest possible definition of state is valid, it’s not very useful for architecting an app.

首先,你不需要管理一些状态(例如纹理),框架本身会替你管理。所以对于状态的更有用的定义是 “当任何时候你需要重建你的用户界面时你所需要的数据”。其次,你需要自己 管理 的状态可以分为两种概念类型:短时 (ephemeral) 状态和应用 (app) 状态。

First, you don’t even manage some state (like textures). The framework handles those for you. So a more useful definition of state is “whatever data you need in order to rebuild your UI at any moment in time”. Second, the state that you do manage yourself can be separated into two conceptual types: ephemeral state and app state.

短时状态

Ephemeral state

短时状态(有时也称 用户界面 (UI) 状态 或者 局部状态)是你可以完全包含在一个独立 widget 中的状态。

Ephemeral state (sometimes called UI state or local state) is the state you can neatly contain in a single widget.

这是一个有点儿模糊的定义,这里有几个例子。

This is, intentionally, a vague definition, so here are a few examples.

widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变。

Other parts of the widget tree seldom need to access this kind of state. There is no need to serialize it, and it doesn’t change in complex ways.

换句话说,不需要使用状态管理架构(例如 ScopedModel, Redux)去管理这种状态。你需要用的只是一个 StatefulWidget

In other words, there is no need to use state management techniques (ScopedModel, Redux, etc.) on this kind of state. All you need is a StatefulWidget.

在下方你可以看到一个底部导航栏中当前被选中的项目是如何被被保存在 _MyHomepageState 类的 _index 变量中。在这个例子中,_index 是一个短时状态。

Below, you see how the currently selected item in a bottom navigation bar is held in the _index field of the _MyHomepageState class. In this example, _index is ephemeral state.

class MyHomepage extends StatefulWidget {
  const MyHomepage({Key? key}) : super(key: key);

  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}

在这里,使用 setState() 和一个在有状态 widget 的 State 类中的变量是很自然的。你的 app 中的其他部分不需要访问 _index。这个变量只会在 MyHomepage widget 中改变。而且,如果用户关闭并重启这个 app,你不会介意 _index 重置回 0.

Here, using setState() and a field inside the StatefulWidget’s State class is completely natural. No other part of your app needs to access _index. The variable only changes inside the MyHomepage widget. And, if the user closes and restarts the app, you don’t mind that _index resets to zero.

应用状态

App state

如果你想在你的应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。

State that is not ephemeral, that you want to share across many parts of your app, and that you want to keep between user sessions, is what we call application state (sometimes also called shared state).

应用状态的一些例子:

Examples of application state:

为了管理应用状态,你需要研究你的选项。你的选择取决于你的应用的复杂度和限制,你的团队之前的经验以及其他方面。请继续阅读。

For managing app state, you’ll want to research your options. Your choice depends on the complexity and nature of your app, your team’s previous experience, and many other aspects. Read on.

没有明确的规则

There is no clear-cut rule

需要说明的是,你 可以 使用 StatesetState() 管理你的应用中的所有状态。实际上Flutter团队在很多简单的示例程序(包括你每次使用 flutter create 命令创建的初始应用)中正是这么做的。

To be clear, you can use State and setState() to manage all of the state in your app. In fact, the Flutter team does this in many simple app samples (including the starter app that you get with every flutter create).

也可以用另外一种方式。比如,在一个特定的应用中,你可以指定底部导航栏中被选中的项目 不是 一个短时状态。你可能需要在底部导航栏类的外部来改变这个值,并在对话期间保留它。在种情况下 _index 就是一个应用状态。

It goes the other way, too. For example, you might decide that—in the context of your particular app—the selected tab in a bottom navigation bar is not ephemeral state. You might need to change it from outside the class, keep it between sessions, and so on. In that case, the _index variable is app state.

没有一个明确、普遍的规则来区分一个变量属于短时状态还是应用状态,有时你不得不在此之间重构。比如,刚开始你认为一些状态是短时状态,但随着应用不断增加功能,有些状态需要被改变为应用状态。

There is no clear-cut, universal rule to distinguish whether a particular variable is ephemeral or app state. Sometimes, you’ll have to refactor one into another. For example, you’ll start with some clearly ephemeral state, but as your application grows in features, it might need to be moved to app state.

因此,请有保留地遵循以下这张流程图:

For that reason, take the following diagram with a large grain of salt:

A flow chart. Start with 'Data'. 'Who needs it?'. Three options: 'Most widgets', 'Some widgets' and 'Single widget'. The first two options both lead to 'App state'. The 'Single widget' option leads to 'Ephemeral state'.

当我们就 React 的 setState 和 Redux 的 Store 哪个好这个问题问 Redux 的作者 Dan Abramov 时, 他如此回答:

When asked about React’s setState versus Redux’s store, the author of Redux, Dan Abramov, replied:

“经验原则是: 选择能够减少麻烦的方式

“The rule of thumb is: Do whatever is less awkward.”

总之,在任何 Flutter 应用中都存在两种概念类型的状态,短时状态经常被用于一个单独 widget 的本地状态,通常使用 StatesetState() 来实现。其他的是你的应用应用状态,在任何一个 Flutter 应用中这两种状态都有自己的位置。如何划分这两种状态取决于你的偏好以及应用的复杂度。

In summary, there are two conceptual types of state in any Flutter app. Ephemeral state can be implemented using State and setState(), and is often local to a single widget. The rest is your app state. Both types have their place in any Flutter app, and the split between the two depends on your own preference and the complexity of the app.

简单的应用状态管理

目录

现在大家已经了解了 声明式的编程思维短时 (ephemeral) 与应用 (app) 状态 之间的区别,现在可以学习如何管理简单的全局应用状态。

Now that you know about declarative UI programming and the difference between ephemeral and app state, you are ready to learn about simple app state management.

在这里,我们打算使用 provider package。如果你是 Flutter 的初学者,而且也没有很重要的理由必须选择别的方式来实现(Redux、Rx、hooks 等等),那么这就是你应该入门使用的。provider 非常好理解而且不需要写很多代码。它也会用到一些在其它实现方式中用到的通用概念。

On this page, we are going to be using the provider package. If you are new to Flutter and you don’t have a strong reason to choose another approach (Redux, Rx, hooks, etc.), this is probably the approach you should start with. The provider package is easy to understand and it doesn’t use much code. It also uses concepts that are applicable in every other approach.

即便如此,如果你已经从其它响应式框架上积累了丰富的状态管理经验的话,那么可以在 状态 (State) 管理参考 中找到相关的 package 和教程。

That said, if you have a strong background in state management from other reactive frameworks, you can find packages and tutorials listed on the options page.

示例

Our example

An animated gif showing a Flutter app in use. It starts with the user on a login screen. They log in and are taken to the catalog screen, with a list of items. The click on several items, and as they do so, the items are marked as "added". The user clicks on a button and gets taken to the cart view. They see the items there. They go back to the catalog, and the items they bought still show "added". End of animation.

为了演示效果,我们实现下面这个简单应用。

For illustration, consider the following simple app.

这个应用有两个独立的页面:一个类别页面和一个购物车页面(分别用 MyCatalogMyCart widget 来展示)。虽然看上去是一个购物应用程序,但是你也可以和社交网络应用类比(把类别页面替换成朋友圈,把购物车替换成关注的人)。

The app has two separate screens: a catalog, and a cart (represented by the MyCatalog, and MyCart widgets, respectively). It could be a shopping app, but you can imagine the same structure in a simple social networking app (replace catalog for “wall” and cart for “favorites”).

类别页面包含一个自定义的 app bar (MyAppBar) 以及一个包含元素列表的可滑动的视图 (MyListItems)。

The catalog screen includes a custom app bar (MyAppBar) and a scrolling view of many list items (MyListItems).

这是应用程序对应的可视化的 widget 树。

Here’s the app visualized as a widget tree.

A widget tree with MyApp at the top, and  MyCatalog and MyCart below it. MyCart area leaf nodes, but MyCatalog have two children: MyAppBar and a list of MyListItems.

所以我们有至少 5 个 Widget 的子类。他们中有很多需要访问一些全局的状态。比如,MyListItem 会被添加到购物车中。但是它可能需要检查和自己相同的元素是否已经被添加到购物车中。

So we have at least 5 subclasses of Widget. Many of them need access to state that “belongs” elsewhere. For example, each MyListItem needs to be able to add itself to the cart. It might also want to see whether the currently displayed item is already in the cart.

这里我们出现了第一个问题:我们把当前购物车的状态放在哪合适呢?

This takes us to our first question: where should we put the current state of the cart?

提高状态的层级

Lifting state up

在 Flutter 中,有必要将存储状态的对象置于 widget 树中对应 widget 的上层。

In Flutter, it makes sense to keep the state above the widgets that use it.

为什么呢?在类似 Flutter 的声明式框架中,如果你想要修改 UI,那么你需要重构它。并没有类似 MyCart.updateWith(somethingNew) 的简单调用方法。换言之,你很难通过外部调用方法修改一个 widget。即便你自己实现了这样的模式,那也是和整个框架不相兼容。

Why? In declarative frameworks like Flutter, if you want to change the UI, you have to rebuild it. There is no easy way to have MyCart.updateWith(somethingNew). In other words, it’s hard to imperatively change a widget from outside, by calling a method on it. And even if you could make this work, you would be fighting the framework instead of letting it help you.

// BAD: DO NOT DO THIS
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

即使你实现了上面的代码,也得处理 MyCart widget 中的代码:

Even if you get the above code to work, you would then have to deal with the following in the MyCart widget:

// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
  return SomeWidget(
    // The initial state of the cart.
  );
}

void updateWith(Item item) {
  // Somehow you need to change the UI from here.
}

你可能需要考虑当前 UI 的状态,然后把最新的数据添加进去。但是这样的方式很难避免出现 bug。

You would need to take into consideration the current state of the UI and apply the new data to it. It’s hard to avoid bugs this way.

在 Flutter 中,每次当 widget 内容发生改变的时候,你就需要构造一个新的。你会调用 MyCart(contents)(构造函数),而不是 MyCart.updateWith(somethingNew)(调用方法)。因为你只能通过父类的 build 方法来构建新 widget,如果你想修改 contents,就需要调用 MyCart 的父类甚至更高一级的类。

In Flutter, you construct a new widget every time its contents change. Instead of MyCart.updateWith(somethingNew) (a method call) you use MyCart(contents) (a constructor). Because you can only construct new widgets in the build methods of their parents, if you want to change contents, it needs to live in MyCart’s parent or above.

// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}

这里 MyCart 可以在各种版本的 UI 中调用同一个代码路径。

Now MyCart has only one code path for building any version of the UI.

// GOOD
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // Just construct the UI once, using the current state of the cart.
    // ···
  );
}

在我们的例子中,contents会存在于 MyApp 的生命周期中。当它发生改变的时候,它会从上层重构 MyCart 。因为这个机制,所以 MyCart 无需考虑生命周期的问题—它只需要针对 contents 声明所需显示内容即可。当内容发生改变的时候,旧的 MyCart widget 就会消失,完全被新的 widget 替代。

和上面的空间树一样,不过我们在 MyApp 的旁边显示一个 'cart' 标记。这里有两个箭头。一个从 MyListItems 指向 'cart',另一个从 'cart' 指向 MyCart

In our example, contents needs to live in MyApp. Whenever it changes, it rebuilds MyCart from above (more on that later). Because of this, MyCart doesn’t need to worry about lifecycle—it just declares what to show for any given contents. When that changes, the old MyCart widget disappears and is completely replaced by the new one.

这就是我们所说的 widget 是不可变的。因为它们会直接被替换。

This is what we mean when we say that widgets are immutable. They don’t change—they get replaced.

现在我们知道在哪里放置购物车的状态,接下来看一下如何读取该状态。

Now that we know where to put the state of the cart, let’s see how to access it.

读取状态

Accessing the state

当用户点击类别页面中的一个元素,它会被添加到购物车里。然而当购物车在 widget 树中,处于 MyListItem 的层级之上时,又该如何访问状态呢?

When a user clicks on one of the items in the catalog, it’s added to the cart. But since the cart lives above MyListItem, how do we do that?

一个简单的实现方法是提供一个回调函数,当 MyListItem 被点击的时候可以调用。 Dart 的函数都是 first class 对象,所以你可以以任意方式传递它们。所以在 MyCatalog 里你可以使用下面的代码:

A simple option is to provide a callback that MyListItem can call when it is clicked. Dart’s functions are first class objects, so you can pass them around any way you want. So, inside MyCatalog you can define the following:

@override
Widget build(BuildContext context) {
  return SomeWidget(
    // Construct the widget, passing it a reference to the method above.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}

这段代码是没问题的,但是对于全局应用状态来说,你需要在不同的地方进行修改,可能需要大量传递回调函数——这些回调很快就会过时。

This works okay, but for an app state that you need to modify from many different places, you’d have to pass around a lot of callbacks—which gets old pretty quickly.

幸运的是 Flutter 在 widget 中存在一种机制,能够为其子孙节点提供数据和服务。(换言之,不仅仅是它的子节点,所有在它下层的 widget 都可以)。就像你所了解的, Flutter 中的 Everything is a Widget™。这里的机制也是一种 widget —InheritedWidget, InheritedNotifier, InheritedModel等等。我们这里不会详细解释他们,因为这些 widget 都太底层。

Fortunately, Flutter has mechanisms for widgets to provide data and services to their descendants (in other words, not just their children, but any widgets below them). As you would expect from Flutter, where Everything is a Widget™, these mechanisms are just special kinds of widgets—InheritedWidget, InheritedNotifier, InheritedModel, and more. We won’t be covering those here, because they are a bit low-level for what we’re trying to do.

我们会用一个 package 来和这些底层的 widget 打交道,就是 provider package。

Instead, we are going to use a package that works with the low-level widgets but is simple to use. It’s called provider.

在使用 provider 之前,请不要忘记在 pubspec.yaml 文件里加入依赖。

Before working with provider, don’t forget to add the dependency on it to your pubspec.yaml.

name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0.0

dev_dependencies:
  # ...

现在可以在代码里加入 import 'package:provider/provider.dart'; 进而开始构建你的应用了/

Now you can import 'package:provider/provider.dart'; and start building.

provider package 中,你无须关心回调或者 InheritedWidgets。但是你需要理解三个概念:

With provider, you don’t need to worry about callbacks or InheritedWidgets. But you do need to understand 3 concepts:

ChangeNotifier

ChangeNotifier 是 Flutter SDK 中的一个简单的类。它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。(这和大家所熟悉的观察者模式相类似)。

ChangeNotifier is a simple class included in the Flutter SDK which provides change notification to its listeners. In other words, if something is a ChangeNotifier, you can subscribe to its changes. (It is a form of Observable, for those familiar with the term.)

provider 中,ChangeNotifier 是一种能够封装应用程序状态的方法。对于特别简单的程序,你可以通过一个 ChangeNotifier 来满足全部需求。在相对复杂的应用中,由于会有多个模型,所以可能会有多个 ChangeNotifier。 (不是必须得把 ChangeNotifierprovider 结合起来用,不过它确实是一个特别简单的类)。

In provider, ChangeNotifier is one way to encapsulate your application state. For very simple apps, you get by with a single ChangeNotifier. In complex ones, you’ll have several models, and therefore several ChangeNotifiers. (You don’t need to use ChangeNotifier with provider at all, but it’s an easy class to work with.)

在我们的购物应用示例中,我们打算用 ChangeNotifier 来管理购物车的状态。我们创建一个新类,继承它,像下面这样:

In our shopping app example, we want to manage the state of the cart in a ChangeNotifier. We create a new class that extends it, like so:

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

唯一一行和 ChangeNotifier 相关的代码就是调用 notifyListeners()。当模型发生改变并且需要更新 UI 的时候可以调用该方法。而剩下的代码就是 CartModel 和它本身的业务逻辑。

The only code that is specific to ChangeNotifier is the call to notifyListeners(). Call this method any time the model changes in a way that might change your app’s UI. Everything else in CartModel is the model itself and its business logic.

ChangeNotifierflutter:foundation 的一部分,而且不依赖 Flutter 中任何高级别类。测试起来非常简单(你都不需要使用 widget 测试)。比如,这里有一个针对 CartModel 简单的单元测试:

ChangeNotifier is part of flutter:foundation and doesn’t depend on any higher-level classes in Flutter. It’s easily testable (you don’t even need to use widget testing for it). For example, here’s a simple unit test of CartModel:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});

ChangeNotifierProvider

ChangeNotifierProvider widget 可以向其子孙节点暴露一个 ChangeNotifier 实例。它属于 provider package。

ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.

我们已经知道了该把 ChangeNotifierProvider 放在什么位置:在需要访问它的 widget 之上。在 CartModel 里,也就意味着将它置于 MyCartMyCatalog 之上。

We already know where to put ChangeNotifierProvider: above the widgets that need to access it. In the case of CartModel, that means somewhere above both MyCart and MyCatalog.

你肯定不愿意把 ChangeNotifierProvider 放的级别太高(因为你不希望破坏整个结构)。但是在我们这里的例子中,MyCartMyCatalog 之上只有 MyApp

You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope). But in our case, the only widget that is on top of both MyCart and MyCatalog is MyApp.

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

请注意我们定义了一个 builder 来创建一个 CartModel 的实例。 ChangeNotifierProvider 非常聪明,它 不会 重复实例化 CartModel,除非在个别场景下。如果该实例已经不会再被调用, ChangeNotifierProvider 也会自动调用 CartModeldispose() 方法。

Note that we’re defining a builder that creates a new instance of CartModel. ChangeNotifierProvider is smart enough not to rebuild CartModel unless absolutely necessary. It also automatically calls dispose() on CartModel when the instance is no longer needed.

如果你想提供更多状态,可以使用 MultiProvider

If you want to provide more than one class, you can use MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}

Consumer

现在 CartModel 已经通过 ChangeNotifierProvider 在应用中与 widget 相关联。我们可以开始调用它了。

Now that CartModel is provided to widgets in our app through the ChangeNotifierProvider declaration at the top, we can start using it.

完成这一步需要通过 Consumer widget。

This is done through the Consumer widget.

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

我们必须指定要访问的模型类型。在这个示例中,我们要访问 CartModel 那么就写上 Consumer<CartModel>

We must specify the type of the model that we want to access. In this case, we want CartModel, so we write Consumer<CartModel>. If you don’t specify the generic (<CartModel>), the provider package won’t be able to help you. provider is based on types, and without the type, it doesn’t know what you want.

Consumer widget 唯一必须的参数就是 builder。当 ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当你在模型中调用 notifyListeners() 时,所有相关的 Consumer widget 的 builder 方法都会被调用。)

The only required argument of the Consumer widget is the builder. Builder is a function that is called whenever the ChangeNotifier changes. (In other words, when you call notifyListeners() in your model, all the builder methods of all the corresponding Consumer widgets are called.)

builder 在被调用的时候会用到三个参数。第一个是 context。在每个 build 方法中都能找到这个参数。

The builder is called with three arguments. The first one is context, which you also get in every build method.

builder 函数的第二个参数是 ChangeNotifier 的实例。它是我们最开始就能得到的实例。你可以通过该实例定义 UI 的内容。

The second argument of the builder function is the instance of the ChangeNotifier. It’s what we were asking for in the first place. You can use the data in the model to define what the UI should look like at any given point.

第三个参数是 child,用于优化目的。如果 Consumer 下面有一个庞大的子树,当模型发生改变的时候,该子树 并不会 改变,那么你就可以仅仅创建它一次,然后通过 builder 获得该实例。

The third argument is child, which is there for optimization. If you have a large widget subtree under your Consumer that doesn’t change when the model changes, you can construct it once and get it through the builder.

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text("Total price: ${cart.totalPrice}"),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

最好能把 Consumer 放在 widget 树尽量低的位置上。你总不希望 UI 上任何一点小变化就全盘重新构建 widget 吧。

It is best practice to put your Consumer widgets as deep in the tree as possible. You don’t want to rebuild large portions of the UI just because some detail somewhere changed.

// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

换成:

Instead:

// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);

Provider.of

有的时候你不需要模型中的 数据 来改变 UI,但是你可能还是需要访问该数据。比如,ClearCart 按钮能够清空购物车的所有商品。它不需要显示购物车里的内容,只需要调用 clear() 方法。

Sometimes, you don’t really need the data in the model to change the UI but you still need to access it. For example, a ClearCart button wants to allow the user to remove everything from the cart. It doesn’t need to display the contents of the cart, it just needs to call the clear() method.

我们可以使用 Consumer<CartModel> 来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget。

We could use Consumer<CartModel> for this, but that would be wasteful. We’d be asking the framework to rebuild a widget that doesn’t need to be rebuilt.

所以这里我们可以使用 Provider.of,并且将 listen 设置为 false

For this use case, we can use Provider.of, with the listen parameter set to false.

Provider.of<CartModel>(context, listen: false).removeAll();

在 build 方法中使用上面的代码,当 notifyListeners 被调用的时候,并不会使 widget 被重构。

Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called.

把代码集成在一起

Putting it all together

你可以在文章中 查看这个示例。如果你想参考稍微简单一点的示例,可以看看 Counter 应用程序是如何 基于 provider 实现的

You can check out the example covered in this article. If you want something simpler, see what the simple Counter app looks like when built with provider.

通过跟着这些文章的学习,你已经大大提高了创建一个包含状态管理应用的能力。试着自己用 provider 构建一个应用来掌握这些技能吧!

By following along with these articles, you’ve greatly improved your ability to create state-based applications. Try building an application with provider yourself to master these skills.

状态 (State) 管理参考

目录

状态管理是一个相当复杂的话题。如果您在浏览后发现一些问题并未得到解答,或者并不适用于您的具体需求场景,自信些,您的实现就是对的。

State management is a complex topic. If you feel that some of your questions haven’t been answered, or that the approach described on these pages is not viable for your use cases, you are probably right.

通过下面的链接了解更多的信息,其中有很多信息都是由社区(第三方)提供。

Learn more at the following links, many of which have been contributed by the Flutter community:

总体概览

General overview

在选择一个具体内容前,您可以先查看以下几项。

Things to review before selecting an approach.

Provider

推荐的管理方式。

A recommended approach.

Riverpod

Riverpod 是另一个不错的选择,它类似于 Provider,并且是编译安全和可测试的。 Riverpod 不依赖于 Flutter SDK。

Riverpod, another good choice, is similar to Provider and is compile-safe and testable. Riverpod doesn’t have a dependency on the Flutter SDK.

setState

适用于较小规模 widget 的暂时性状态的基础管理方法。

The low-level approach to use for widget-specific, ephemeral state.

InheritedWidget & InheritedModel

Widget tree 中不同层级间的 widget 通信的基础方法。这是诸如 provider 等众多方法的底层实现。

The low-level approach used to communicate between ancestors and children in the widget tree. This is what provider and many other approaches use under the hood.

以下讲师指导的视频 workshop 介绍了如何使用 InheritedWidget

The following instructor-led video workshop covers how to use InheritedWidget:

其他有用的文档包括:

Other useful docs include:

Redux

前端开发者较为熟悉的状态容器实现。

A state container approach familiar to many web developers.

Fish-Redux

Fish Redux 是一个基于 Redux 状态管理的组合式 Flutter 应用框架,适用于构建中型和大型应用。

Fish Redux is an assembled flutter application framework based on Redux state management. It is suitable for building medium and large applications.

BLoC / Rx

基于流/观察者模式的系列。

A family of stream/observable based patterns.

GetIt

A service locator based state management approach that doesn’t need a BuildContext.

MobX

一个基于观察及响应的状态管理常用库。

A popular library based on observables and reactions.

Flutter Commands

基于 ValueNotifiers 的命令式的状态管理,能与 GetIt 完美结合使用,也可以与 Provider 或者其他 locators 配合使用。

Reactive state management that uses the Command Pattern and is based on ValueNotifiers. Best in combination with GetIt, but can be used with Provider or other locators too.

Binder

一个使用 InheritedWidget 作为核心实现的状态管理库。受到 recoil 的启发,该库提供了分治的解决方式。

A state management package that uses InheritedWidget at its core. Inspired in part by recoil. This package promotes the separation of concerns.

GetX

一个简单的响应式状态管理解决方案。

A simplified reactive state management solution.

states_rebuilder

一种将状态管理与依赖注入解决方案和集成路由器相结合的方法。更多信息,请参阅以下信息:

An approach that combines state management with a dependency injection solution and an integrated router. For more information, see the following info:

Triple Pattern (Segmented State Pattern)

Triple is a pattern for state management that uses Streams or ValueNotifier. This mechanism (nicknamed triple because the stream always uses three values: Error, Loading, and State), is based on the Segmented State pattern.

For more information, refer to the following resources:

网络

目录

Cross-platform http networking

The http package provides the simplest way to issue http requests. This package is supported on Android, iOS, and the web.

Platform notes

Some platforms require additional steps, as detailed below.

Android

Android apps must declare their use of the internet in the Android manifest (AndroidManifest.xml ):

<manifest xmlns:android...>
 ...
 <uses-permission android:name="android.permission.INTERNET" />
 <application ...
</manifest>

Samples

For a practical sample of various networking tasks (incl. fetching data, WebSockets, and parsing data in the background) see the networking cookbook.

JSON 和序列化数据

目录

大多数移动应用都需要与 web 服务器通信,同时在某些时候轻松地存储结构化数据。当创造需要网络连接的应用时,它迟早需要处理一些常见的 JSON。

It is hard to think of a mobile app that doesn’t need to communicate with a web server or easily store structured data at some point. When making network-connected apps, the chances are that it needs to consume some good old JSON, sooner or later.

本指南介绍了如何在 Flutter 中使用 JSON。包括了如何在不同场景中使用相应的 JSON 解决方案,以及为什么要这么做。

This guide looks into ways of using JSON with Flutter. It covers which JSON solution to use in different scenarios, and why.

我需要哪一种 JSON 序列化数据方法?

Which JSON serialization method is right for me?

本文涵盖了两种常规的 JSON 使用策略:

This article covers two general strategies for working with JSON:

不同的项目复杂度不同,用例也不一样。对于较小的概念验证项目或者快速原型,使用代码生成器可能有些过于繁杂。对于具有很多更加复杂的 JSON 模型的应用,手动编码可能很快变得无聊、重复并且出现很多小错误。

Different projects come with different complexities and use cases. For smaller proof-of-concept projects or quick prototypes, using code generators might be overkill. For apps with several JSON models with more complexity, encoding by hand can quickly become tedious, repetitive, and lend itself to many small errors.

为较小的项目使用手动序列化数据

Use manual serialization for smaller projects

手动 JSON 解码是指在 dart:convert 中使用内置的 JSON 解码器。它包括将原始 JSON 字符串传递给 jsonDecode() 方法,然后在产生的 Map<String, dynamic> 计算结果中寻找你需要的值。它没有外部依赖或者特定的设置过程,这有利于快速证明概念。

Manual JSON decoding refers to using the built-in JSON decoder in dart:convert. It involves passing the raw JSON string to the jsonDecode() function, and then looking up the values you need in the resulting Map<String, dynamic>. It has no external dependencies or particular setup process, and it’s good for a quick proof of concept.

当你的项目变大时,手动解码表现得并不理想。手动编写解码逻辑会变得难以管理并容易出错。如果你产生了笔误去获取一个不存在的 JSON 字段,你的代码会在运行时抛出一个错误。

Manual decoding does not perform well when your project becomes bigger. Writing decoding logic by hand can become hard to manage and error-prone. If you have a typo when accessing a nonexistent JSON field, your code throws an error during runtime.

如果你的项目没有很多的 JSON 模型并且你正在寻找一个快速测试概念的方法,手动序列化数据可能是你要的开始的方式。关于手动编码的示例,请参阅 使用 dart:convert 手动序列化 JSON 数据

If you do not have many JSON models in your project and are looking to test a concept quickly, manual serialization might be the way you want to start. For an example of manual encoding, see Serializing JSON manually using dart:convert.

为中大型项目使用代码生成

Use code generation for medium to large projects

利用代码生成的 JSON 序列化数据,意味着可以通过外部的库生成编码模板。在一些初始化设置后,你可以运行文件监听程序,来从你的模型类生成代码。例如,json_serializablebuilt_value 就是这类的库。

JSON serialization with code generation means having an external library generate the encoding boilerplate for you. After some initial setup, you run a file watcher that generates the code from your model classes. For example, json_serializable and built_value are these kinds of libraries.

这种方法适用于大型项目。不需要手动编写模板,并且一些试图去获取不存在的 JSON 字段的笔误,会在编译阶段被发现。代码生成的麻烦之处,在于它需要一些初始化设置。并且,生成的源文件可能在你的项目导航中产生一些视觉上的混乱。

This approach scales well for a larger project. No hand-written boilerplate is needed, and typos when accessing JSON fields are caught at compile-time. The downside with code generation is that it requires some initial setup. Also, the generated source files might produce visual clutter in your project navigator.

当你有一个中大型项目时,你可能想要使用生成的代码来进行 JSON 序列化。要看基于代码生成的 JSON 编码,见 使用代码生成库序列化 JSON 数据

You might want to use generated code for JSON serialization when you have a medium or a larger project. To see an example of code generation based JSON encoding, see Serializing JSON using code generation libraries.

Flutter 中是否有 GSON/Jackson/Moshi 的等价物?

Is there a GSON/Jackson/Moshi equivalent in Flutter?

简单来说,没有。

The simple answer is no.

这样的库需要使用运行时进行 反射,这在 Flutter 中是被禁用的。运行时反射会影响 Dart 支持了相当久的 摇树优化。通过 tree shaking,你可以从你的发布版本中“抖掉”不需要使用的代码。这会显著优化 App 的体积。

Such a library would require using runtime reflection, which is disabled in Flutter. Runtime reflection interferes with tree shaking, which Dart has supported for quite a long time. With tree shaking, you can “shake off” unused code from your release builds. This optimizes the app’s size significantly.

由于反射会默认让所有的代码被隐式使用,这让 tree shaking 变得困难。工具不知道哪一部分在运行时不会被用到,所以冗余的代码很难被清除。当使用反射时,App 的体积不能被轻易优化。

Since reflection makes all code implicitly used by default, it makes tree shaking difficult. The tools cannot know what parts are unused at runtime, so the redundant code is hard to strip away. App sizes cannot be easily optimized when using reflection.

尽管你不能在 Flutter 中使用运行时反射,还是有一些库提供了基于代码生成的方便使用的 API,这个方法的更多细节在 代码生成库 部分。

Although you cannot use runtime reflection with Flutter, some libraries give you similarly easy-to-use APIs but are based on code generation instead. This approach is covered in more detail in the code generation libraries section.

使用 dart:convert 手动序列化 JSON 数据

Serializing JSON manually using dart:convert

在 Flutter 中基础的序列化 JSON 十分容易的。 Flutter 有一个内置的 dart:convert 的库,这个库包含了一个简单的 JSON 编码器和解码器。

Basic JSON serialization in Flutter is very simple. Flutter has a built-in dart:convert library that includes a straightforward JSON encoder and decoder.

下面的样例实现了一个简单用户模型。

The following sample JSON implements a simple user model.

{
  "name": "John Smith",
  "email": "john@example.com"
}

通过 dart:convert,你可以用两种方法编码这个 JSON 模型。

With dart:convert, you can serialize this JSON model in two ways.

内联序列化 JSON 数据

Serializing JSON inline

通过查阅 dart:convert 文档,你会看到你可以将 JSON 字符串作为方法的参数,调用 jsonDecode() 方法来解码 JSON。

By looking at the dart:convert documentation, you’ll see that you can decode the JSON by calling the jsonDecode() function, with the JSON string as the method argument.

Map<String, dynamic> user = jsonDecode(jsonString);

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

不幸的是,jsonDecode() 返回一个 Map<String, dynamic>,这意味着你在运行时以前都不知道值的类型。使用这个方法,你失去了大部分的静态类型语言特性:类型安全、自动补全以及最重要的编译时异常。你的代码会立即变得更加容易出错。

Unfortunately, jsonDecode() returns a Map<String, dynamic>, meaning that you do not know the types of the values until runtime. With this approach, you lose most of the statically typed language features: type safety, autocompletion and most importantly, compile-time exceptions. Your code will become instantly more error-prone.

例如,当你获取 name 或者 email 字段,你可能很快引入一个笔误。然而编译器却无法知道映射中是否有 JSON 笔误。

For example, whenever you access the name or email fields, you could quickly introduce a typo. A typo that the compiler doesn’t know about since the JSON lives in a map structure.

在模型类中序列化 JSON 数据

Serializing JSON inside model classes

通过引入一个简单的模型 User 类来解决上面提到的问题。在 User 类中,你会发现:

Combat the previously mentioned problems by introducing a plain model class, called User in this example. Inside the User class, you’ll find:

通过这种方法,调用代码 可以拥有类型安全、 nameemail 字段的自动完成以及编译时异常检测。如果你不小心写错了,或者把 String 类型的字段看成了 int 类型,应用将无法编译,而不是在运行时崩溃。

With this approach, the calling code can have type safety, autocompletion for the name and email fields, and compile-time exceptions. If you make typos or treat the fields as ints instead of Strings, the app won’t compile, instead of crashing at runtime.

user.dart

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() => {
        'name': name,
        'email': email,
      };
}

解码逻辑的责任现在转移到了模型内部。通过这个新方法,你可以很容易地解码获得一个 user 实例。

The responsibility of the decoding logic is now moved inside the model itself. With this new approach, you can decode a user easily.

Map<String, dynamic> userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');

要对 user 实例进行编码,将 User 对象传到 jsonEncode() 函数中。你不需要调用 toJson() 方法,因为 jsonEncode() 已经帮你做了这件事。

To encode a user, pass the User object to the jsonEncode() function. You don’t need to call the toJson() method, since jsonEncode() already does it for you.

String json = jsonEncode(user);

通过这种方法,被调用的代码根本不需要担心序列化 JSON 数据的问题。然而,你仍然需要模型类。你当然会希望序列化数据在一个生产环境的应用里能奏效。在实践中,User.fromJson()User.toJson() 方法都需要单元测试以便验证正确的行为。

With this approach, the calling code doesn’t have to worry about JSON serialization at all. However, the model class still definitely has to. In a production app, you would want to ensure that the serialization works properly. In practice, the User.fromJson() and User.toJson() methods both need to have unit tests in place to verify correct behavior.

然而,现实场景通常不是那么简单,有时候响应的 JSON API 会更加复杂,例如它可能会包含一些相邻的 JSON 对象,而这些对象同样需要使用它的 model 类进行解析。

However, real-world scenarios are not usually that simple. Sometimes JSON API responses are more complex, for example since they contain nested JSON objects that must be parsed through their own model class.

如果有一些东西可以帮你处理 JSON 编码和解码就好了。幸运的是,已经有了!

It would be nice if there were something that handled the JSON encoding and decoding for you. Luckily, there is!

使用代码生成库序列化 JSON 数据

Serializing JSON using code generation libraries

尽管有其它库可以使用,但是本指南使用了 json_serializable,一个自动化源代码生成器来为你生成 JSON 序列化数据模板。

Although there are other libraries available, this guide uses json_serializable, an automated source code generator that generates the JSON serialization boilerplate for you.

由于序列化数据代码不再需要手动编写或者维护,你可以将序列化 JSON 数据在运行时的异常风险降到最低。

Since the serialization code is not handwritten or maintained manually anymore, you minimize the risk of having JSON serialization exceptions at runtime.

在项目中设置 json_serializable

Setting up json_serializable in a project

要在你的项目中包含 json_serializable,你需要一个常规依赖,以及两个 dev 依赖。简单来说,dev 依赖 是不包括在我们的 App 源代码中的依赖——它们只会被用在开发环境中。

To include json_serializable in your project, you need one regular dependency, and two dev dependencies. In short, dev dependencies are dependencies that are not included in our app source code—they are only used in the development environment.

在序列化 JSON 数据的例子中,这些需要的依赖的最新版本可以在 pubspec 文件 中查看。

The latest versions of these required dependencies can be seen by following the pubspec file in the JSON serializable example.

pubspec.yaml

dependencies:
  # Your other regular dependencies here
  json_annotation: <latest_version>

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: <latest_version>
  json_serializable: <latest_version>

在你的项目根文件夹下运行 flutter pub get (或者在你的编辑器中点击 Packages Get)以确保在你的项目中可以使用这些新的依赖。

Run flutter pub get inside your project root folder (or click Packages get in your editor) to make these new dependencies available in your project.

以 json_serializable 的方式创建模型类

Creating model classes the json_serializable way

下面显示了怎样将 User 类转换为 json_serializable 后的类。简单起见,该代码使用了前面的例子中的简化的 JSON 模型。

The following shows how to convert the User class to a json_serializable class. For the sake of simplicity, this code uses the simplified JSON model from the previous samples.

user.dart

import 'package:json_annotation/json_annotation.dart';

/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';

/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()
class User {
  User(this.name, this.email);

  String name;
  String email;

  /// A necessary factory constructor for creating a new User instance
  /// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
  /// The constructor is named after the source class, in this case, User.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` is the convention for a class to declare support for serialization
  /// to JSON. The implementation simply calls the private, generated
  /// helper method `_$UserToJson`.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

通过这个设置,源代码生成器将生成用于 JSON 编码及解码 name 以及 email 字段的代码。

With this setup, the source code generator generates code for encoding and decoding the name and email fields from JSON.

如果需要,你可以很轻易地自定义命名策略。例如,如果 API 返回带有 蛇形命名方式 的对象,并且你想要在你的模型里使用 小驼峰 的命名方式,你可以使用带有一个 name 参数的 @JsonKey 注解。

If needed, it is also easy to customize the naming strategy. For example, if the API returns objects with snake_case, and you want to use lowerCamelCase in your models, you can use the @JsonKey annotation with a name parameter:

/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

客户端和服务端最好保持同样的命名规则。 @JsonSerializable() 提供了 fieldRename 枚举,用于将 dart 字段完整转换为 JSON 键值。

It’s best if both server and client follow the same naming strategy.
@JsonSerializable() provides fieldRename enum for totally converting dart fields into JSON keys.

定义 @JsonSerializable(fieldRename: FieldRename.snake) 与添加 @JsonKey(name: '<snake_case>') 到每一个字段是同样的效果。

Modifying @JsonSerializable(fieldRename: FieldRename.snake) is equivalent to adding @JsonKey(name: '<snake_case>') to each field.

服务端的数据有时无法确认,所以在客户端很有必要进行数据校验和保护。其他常见的 @JsonKey 声明方法包括:

Sometimes server data is uncertain, so it is necessary to verify and protect data on client.
Other commonly used @JsonKey annotations include:

/// Tell json_serializable to use "defaultValue" if the JSON doesn't
/// contain this key or if the value is `null`.
@JsonKey(defaultValue: false)
final bool isAdult;

/// When `true` tell json_serializable that JSON must contain the key, 
/// If the key doesn't exist, an exception is thrown.
@JsonKey(required: true)
final String id;

/// When `true` tell json_serializable that generated code should 
/// ignore this field completely. 
@JsonKey(ignore: true)
final String verificationCode;

运行代码生成工具

Running the code generation utility

当你首次创建 json_serializable 类时,你会得到类似下图的错误。

When creating json_serializable classes the first time, you’ll get errors similar to what is shown in the image below.

IDE warning when the generated code for a model class does not exist
yet.

这些错误完全正常,很简单,因为这些模型类的生成代码并不存在。要解决这个问题,你需要运行代码生成器来生成序列化数据模板。

These errors are entirely normal and are simply because the generated code for the model class does not exist yet. To resolve this, run the code generator that generates the serialization boilerplate.

有两种方式运行代码生成器。

There are two ways of running the code generator.

一次性代码生成

One-time code generation

通过在项目根目录运行 flutter pub run build_runner build,你可以在任何需要的时候为你的模型生成 JSON 序列化数据代码。这会触发一次构建,遍历源文件,选择相关的文件,然后为它们生成必须的序列化数据代码。

By running flutter pub run build_runner build in the project root, you generate JSON serialization code for your models whenever they are needed. This triggers a one-time build that goes through the source files, picks the relevant ones, and generates the necessary serialization code for them.

虽然这样很方便,但是如果你不需要在每次修改了你的模型类后都要手动构建那将会很棒。

While this is convenient, it would be nice if you did not have to run the build manually every time you make changes in your model classes.

持续生成代码

Generating code continuously

监听器 让我们的源代码生成过程更加方便。它会监听我们项目中的文件变化,并且会在需要的时候自动构建必要的文件。你可以在项目根目录运行 flutter pub run build_runner watch 启动监听。

A watcher makes our source code generation process more convenient. It watches changes in our project files and automatically builds the necessary files when needed. Start the watcher by running flutter pub run build_runner watch in the project root.

启动监听并让它留在后台运行是安全的。

It is safe to start the watcher once and leave it running in the background.

使用 json_serializable 模型

Consuming json_serializable models

为了以 json_serializable 的方式解码 JSON 字符串,你不必对以前的代码做任何的改动。

To decode a JSON string the json_serializable way, you do not have actually to make any changes to our previous code.

Map<String, dynamic> userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

编码也是如此。调用 API 和以前一样。

The same goes for encoding. The calling API is the same as before.

String json = jsonEncode(user);

在使用了 json_serializable 后,你可以立马忘掉 User 类中所有手动序列化的 JSON 数据。源代码生成器会创建一个名为 user.g.dart 的文件,它包含了所有必须的序列化数据逻辑。你不必再编写自动化测试来确保序列化数据奏效。现在 由库来负责 确保序列化数据能正确地被转换。

With json_serializable, you can forget any manual JSON serialization in the User class. The source code generator creates a file called user.g.dart, that has all the necessary serialization logic. You no longer have to write automated tests to ensure that the serialization works—it’s now the library’s responsibility to make sure the serialization works appropriately.

为嵌套类 (Nested Classes) 生成代码

Generating code for nested classes

你可能类在代码中用了嵌套类,在你把类作为参数传递给一些服务(比如 Firebase)的时候,你可能会遇到 Invalid argument 错误。

You might have code that has nested classes within a class. If that is the case, and you have tried to pass the class in JSON format as an argument to a service (such as Firebase, for example), you might have experienced an Invalid argument error.

比如下面的这个 Address 类:

Consider the following Address class:

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

@JsonSerializable()
class Address {
  String street;
  String city;

  Address(this.street, this.city);

  factory Address.fromJson(Map<String, dynamic> json) =>
      _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

一个 Address 类被嵌套在 User 类中使用:

The Address class is nested inside the User class:

import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  User(this.name, this.address);

  String name;
  Address address;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

在终端中运行 flutter pub run build_runner build 创建 *.g.dart文件,但私有函数 _$UserToJson() 看起来会像下面这样:

Running flutter pub run build_runner build in the terminal creates the *.g.dart file, but the private _$UserToJson() function looks something like the following:

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
  'name': instance.name,
  'address': instance.address,
};

现在看起来并没有什么问题,但当你想要打印 (print()) 这个用户对象时:

All looks fine now, but if you do a print() on the user object:

Address address = Address("My st.", "New York");
User user = User("John", address);
print(user.toJson());

结果会是:

The result is:

{name: John, address: Instance of 'address'}

而你期望的输出结果是这样的:

When what you probably want is output like the following:

{name: John, address: {street: My st., city: New York}}

为了得到正常的输出,你需要在类声明之前在 @JsonSerializable 方法加入 explicitToJson: true 参数, User 类现在看起来是这样的:

To make this work, pass explicitToJson: true in the @JsonSerializable() annotation over the class declaration. The User class now looks as follows:

import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  User(this.name, this.address);

  String name;
  Address address;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

了解更多信息,请查阅 json_annotation 这个 package 里的 JsonSerializable 类的 explicitToJson 参数等相关文档。

For more information, see explicitToJson in the JsonSerializable class for the json_annotation package.

进一步参考

Further references

更多信息,请查看以下资源:

For more information, see the following resources:

使用 Firestore 调用 Firebase 服务

Firebase 是一个用于开发 BaaS 应用的开发平台,它提供了诸如实时数据库、云存储、鉴权、Crash 上报、机器学习、远程配置以及托管你的静态文件等后台托管服务。

Firebase is a Backend-as-a-Service (BaaS) app development platform that provides hosted backend services such as a realtime database, cloud storage, authentication, crash reporting, machine learning, remote configuration, and hosting for your static files.

Firebase 可以支持 Flutter,如果你想获取更多信息,可以查阅下面这些链接:

Firebase supports Flutter. For more information, see:

当然,Flutter 社区也创建了一些可能会对你有用的文档:

Also, the Flutter community has created docs and videos that you might find useful. Here are a few:

无障碍

目录

确保你的应用能够被广泛的用户使用是构建高质量应用程序至关重要的一点。如果你的应用设计不佳,可能会无法覆盖到所有年龄段的人。 联合国关于残疾人权利 规定了道德和法律必须确保信息系统能够普遍使用。世界各地也都要求提供无障碍的环境;同样,公司也认识到了最大限度覆盖服务的优势所在。

Ensuring apps are accessible to a broad range of users is an essential part of building a high-quality app. Applications that are poorly designed create barriers to people of all ages. The UN Convention on the Rights of Persons with Disabilities states the moral and legal imperative to ensure universal access to information systems; countries around the world enforce accessibility as a requirement; and companies recognize the business advantages of maximizing access to their services.

我们强烈建议你将辅助功能清单添加到发布应用前的关键指标。 Flutter 始终致力于支持开发者能够使它的应用更易于访问,其中就包括了由底层操作系统提供的一流的无障碍支持,包括:

We strongly encourage you to include an accessibility checklist as a key criteria before shipping your app. Flutter is committed to supporting developers in making their apps more accessible, and includes first-class framework support for accessibility in addition to that provided by the underlying operating system, including:

大字体
使用用户指定的字体大小呈现文本 widget

Large fonts
Render text widgets with user-specified font sizes

读屏器
通过语音反馈传达用户界面的内容

Screen readers
Communicate spoken feedback about UI contents

高对比度
在渲染 widget 时,使用具有高对比度的颜色

Sufficient contrast
Render widgets with colors that have sufficient contrast

详细关于这个特性的讨论,请继续阅读。

Details of these features are discussed below.

辅助功能检测

Inspecting accessibility support

关于无障碍功能检测的细节,我们将在下面讨论。除了测试这些特定主题外,我们还建议您使用自动辅助功能扫描程序:

Details of these are discussed below. In addition to testing for these specific topics, we recommend using automated accessibility scanners:

大字体

Large fonts

Android 和 iOS 都包含配置应用程序所需字体大小的系统设置。在确定字体大小时, Flutter 文本 widget 会遵循当前系统设置。

Both Android and iOS contain system settings to configure the desired font sizes used by apps. Flutter text widgets respect this OS setting when determining font sizes.

给开发者的提示

Tips for developers

Flutter 会根据操作系统设置自动计算字体大小。但是,作为开发人员,你应确保在增加字体大小时,你的页面有足够的空间来呈现其所有内容。例如,你可以在小屏幕上设置最大的字体来测试你应用上的全部内容。

Font sizes are calculated automatically by Flutter based on the OS setting. However, as a developer you should make sure your layout has enough room to render all its contents when the font sizes are increased. For example, you can test all parts of your app on a small-screen device configured to use the largest font setting.

例子

Example

以下两个屏幕截图分别显示了使用默认 iOS 字体设置呈现的标准 Flutter 应用程序,和使用 iOS 辅助功能设置中选择的最大字体设置呈现的 Flutter 应用程序。

The following two screenshots show the standard Flutter app template rendered with the default iOS font setting, and with the largest font setting selected in iOS accessibility settings.

Default font setting
Default font setting
Largest accessibility font setting
Largest accessibility font setting

读屏器

Screen readers

对于移动设备,读屏器(TalkBackVoiceOver)可以使视障用户能够获得有关屏幕内容的语音反馈,并通过移动设备上的手势和桌面上的键盘快捷键与 UI 进行交互。在你的移动设备上打开 VoiceOver 或 TalkBack 并浏览你的应用程序。

For mobile, screen readers (TalkBack, VoiceOver) enable visually impaired users to get spoken feedback about the contents of the screen and interact with the UI via gestures on mobile and keyboard shortcuts on desktop. Turn on VoiceOver or TalkBack on your mobile device and navigate around your app.

对于 Web,目前支持以下屏幕阅读器:

For web, the following screen readers are currently supported:

移动浏览器:

Mobile Browsers:

桌面浏览器:

Desktop Browsers:

Web 上的读屏器用户需要点击「启用辅助功能」按钮来构建语义树。如果你使用下面这个 API 以编程方式为你的应用程序自动启用辅助功能,则用户可以跳过此步骤:

Screen Readers users on web will need to toggle “Enable accessibility” button to build the semantics tree. Users can skip this step if you programmatically auto-enable accessibility for your app using this API:

RendererBinding.instance.setSemanticsEnabled(true)

查看此 视频演示,了解 Victor Tsaran,他领导了 Material Design 的辅助功能计划,并在 Flutter Gallery Web 应用程序中使用了 VoiceOver。

Check out this video demo to see Victor Tsaran, who leads the Accessibility program for Material Design, using VoiceOver with the Flutter Gallery web app.

Flutter 的标准 widget 会自动生成无障碍树。但是,如果你的应用需要不同的东西,则可以使用 语义小部件 来自定义您应用程序的无障碍体验。

Flutter’s standard widgets generate an accessibility tree automatically. However, if your app needs something different, it can be customized using the Semantics widget.

高对比度

Sufficient contrast

高对比度能够使文本和图像更易于阅读。除了使具有各种视觉障碍的用户受益外,高对比度也能够帮助所有用户在极端光照条件下 (例如在直射阳光下或在低亮度显示器上) 观看设备上的界面。

Sufficient color contrast makes text and images easier to read. Along with benefitting users with various visual impairments, sufficient color contrast helps all users when viewing an interface on devices in extreme lighting conditions, such as when exposed to direct sunlight or on a display with low brightness.

W3C 建议:

The W3C recommends:

给开发者的提示

Tips for developers

确保你包含的任何图像都具有较高的对比度。

Make sure any images you include have sufficient contrast.

在 widget 上指定颜色时,请确保在前景色和背景色之间具备足够的对比度。

When specifying colors on widgets, make sure sufficient contrast is used between foreground and background color selections.

思考如何构建无障碍应用

Building with accessibility in mind

确保你的应用能够被所有人使用,这意味着你需要从一开始就考虑到无障碍。对于一些应用,说起来容易做起来难。在下面的视频中,我们的两名工程师从一个无障碍状态中获取了一个 Flutter 内置的 widget,以提供更加便捷的体验。

Ensuring your app can be used by everyone means building accessibility into it from the start. For some apps, that’s easier said than done. In the video below, two of our engineers take a mobile app from a dire accessibility state to one that takes advantage of Flutter’s built-in widgets to offer a dramatically more accessible experience.

在 Web 上测试无障碍:

Testing accessibility on web:

您可以通过在配置文件和发布模式下使用以下命令行标志可视化为你的 Web 应用程序创建的语义节点来调试无障碍:

You can debug accessibility by visualizing the semantic nodes created for your web app using the following command line flag in profile and release modes:

$ flutter run -d chrome --profile \ --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true

标记激活后,语义节点出现在 widget 的顶部;你可以验证语义元素是否放置在应有的位置。如果语义节点放置不正确,请 提交错误报告

With the flag activated, the semantic nodes appear on top of the widgets; you can verify that the semantic elements are placed where they should be. If the semantic nodes are incorrectly placed, please file a bug report.

无障碍发布清单

Accessibility release checklist

这里是一些应用发布前的你需要考虑的部分清单。

Here is a non-exhaustive list of things to consider as you prepare your app for release.

更多信息

More information

如果你希望了解更多,尤其是如何配置 semantics tree,请查看如下社区成员贡献的文章:

For more information, particularly about how to configure the semantics tree, see the following articles written by community members:

Flutter 应用里的国际化

目录

如果你的 app 会部署给说其他语言的用户使用,那么你就需要对它进行国际化。这就意味着你在编写 app 的时候,需要采用一种容易对它进行本地化的方式进行开发,这种方式让你能够为每一种语言或者 app 所支持的语言环境下的文本和布局等进行本地化。 Flutter 提供了 widgets 和类来帮助开发者进行国际化,当然 Flutter 库本身就是国际化的。

If your app might be deployed to users who speak another language then you’ll need to internationalize it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. Flutter provides widgets and classes that help with internationalization and the Flutter libraries themselves are internationalized.

由于大多数应用程序都是以这种方式编写的,因此该页面主要介绍了使用 MaterialAppCupertinoApp 对 Flutter 应用程序进行本地化所需的概念和工作流程。但是,使用较低级别的 WidgetsApp 类编写的应用程序也可以使用相同的类和逻辑进行国际化。

This page covers concepts and workflows necessary to localize a Flutter application using the MaterialApp and CupertinoApp classes, as most apps are written that way. However, applications written using the lower level WidgetsApp class can also be internationalized using the same classes and logic.

Flutter 应用本地化介绍

Introduction to localizations in Flutter

本节主要介绍如何对 Flutter 应用进行国际化,以及针对目标平台需要设置的其他内容。

This section provides a tutorial on how to internationalize a Flutter application, along with any additional setup that a target platform might require.

配置一个国际化的 app:flutter_localizations package

Setting up an internation­alized app: the Flutter_localizations package

默认情况下,Flutter 只提供美式英语的本地化。如果想要添加其他语言,你的应用必须指定额外的 MaterialApp 或者 CupertinoApp 属性并且添加一个名为 flutter_localizations 的 package。截至到 2020 年 11 月份,这个 package 已经支持大约 78 种语言。

By default, Flutter only provides US English localizations. To add support for other languages, an application must specify additional MaterialApp (or CupertinoApp) properties, and include a package called flutter_localizations. As of November 2020, this package supports 78 languages.

想要使用 flutter_localizations 的话,你需要在 pubspec.yaml 文件中添加它作为依赖:

To use flutter_localizations, add the package as a dependency to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations: # Add this line
    sdk: flutter         # Add this line

下一步,先运行 pub get packages,然后引入 flutter_localizations 库,然后为 MaterialApp 指定 localizationsDelegatessupportedLocales

Next, run pub get packages, then import the flutter_localizations library and specify localizationsDelegates and supportedLocales for MaterialApp:

import 'package:flutter_localizations/flutter_localizations.dart';
return const MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', ''), // English, no country code
    Locale('es', ''), // Spanish, no country code
  ],
  home: MyHomePage(),
);

引入 flutter_localizations package 并添加了上面的代码之后, MaterialCupertino 包现在应该被正确地本地化为 78 个受支持的语言环境之一。 widget 应当与本地化信息保持同步,并具有正确的从左到右或从右到左的布局。

After introducing the flutter_localizations package and adding the code above, the Material and Cupertino packages should now be correctly localized in one of the 78 supported locales. Widgets should be adapted to the localized messages, along with correct left-to-right and right-to-left layout.

你可以尝试将目标平台的语言环境切换为西班牙语(es),然后应该可以发现信息已经被本地化了。

Try switching the target platform’s locale to Spanish (es) and notice that the messages should be localized.

基于 WidgetsApp 构建的 app 在添加语言环境时,除了 GlobalMaterialLocalizations.delegate 不需要之外,其他的操作是类似的。

Apps based on WidgetsApp are similar except that the GlobalMaterialLocalizations.delegate isn’t needed.

虽然 语言环境 (Locale) 默认的构造函数是完全没有问题的,但是还是建议大家使用 Locale.fromSubtags 的构造函数,因为它支持设置 文字代码

The full Locale.fromSubtags constructor is preferred as it supports scriptCode, though the Locale default constructor is still fully valid.

localizationDelegates 数组是用于生成本地化值集合的工厂。 GlobalMaterialLocalizations.delegate 为 Material 组件库提供本地化的字符串和一些其他的值。 GlobalWidgetsLocalizations.delegate 为 widgets 库定义了默认的文本排列方向,由左到右或者由右到左。

The elements of the localizationsDelegates list are factories that produce collections of localized values. GlobalMaterialLocalizations.delegate provides localized strings and other values for the Material Components library. GlobalWidgetsLocalizations.delegate defines the default text direction, either left-to-right or right-to-left, for the widgets library.

想知道更多关于这些 app 属性,它们依赖的类型以及那些国际化的 Flutter app 通常是如何组织的,可以继续阅读下面内容。

More information about these app properties, the types they depend on, and how internationalized Flutter apps are typically structured, can be found below.

添加您自己的本地化信息

Adding your own localized messages

引入 flutter_localizations package 后,请按照以下说明将本地化的文本添加到您的应用程序。

Once the flutter_localizations package is added, use the following instructions to add localized text to your application.

  1. intl package 添加到 pubspec.yaml 文件中:

    Add the intl package to the pubspec.yaml file:

    dependencies:
      flutter:
        sdk: flutter
      flutter_localizations:
        sdk: flutter
      intl: ^0.17.0 # Add this line
  2. 另外,在 pubspec.yaml 文件中,启用 generate 标志。该设置项添加在 pubspec 中 Flutter 部分,通常处在 pubspec 文件中后面的部分。

    Also, in the pubspec.yaml file, enable the generate flag. This is added to the section of the pubspec that is specific to Flutter, and usually comes later in the pubspec file.

    # The following section is specific to Flutter.
    flutter:
      generate: true # Add this line
  3. 在 Flutter 项目的根目录中添加一个新的 yaml 文件,命名为 l10n.yaml,其内容如下:

    Add a new yaml file to the root directory of the Flutter project called l10n.yaml with the following content:

    arb-dir: lib/l10n
    template-arb-file: app_en.arb
    output-localization-file: app_localizations.dart

    该文件用于配置本地化工具;在上面的示例中,指定输入文件在 ${FLUTTER_PROJECT}/lib/l10n 中,app_en.arb 文件提供模板,生成的本地化文件在 app_localizations.dart 文件中。

    This file configures the localization tool; in this example, the input files are located in ${FLUTTER_PROJECT}/lib/l10n, the app_en.arb file provides the template, and the generated localizations are placed in the app_localizations.dart file.

  4. ${FLUTTER_PROJECT}/lib/l10n 中,添加 app_en.arb 模板文件。如下:

    In ${FLUTTER_PROJECT}/lib/l10n, add the app_en.arb template file. For example:

    {
        "helloWorld": "Hello World!",
        "@helloWorld": {
          "description": "The conventional newborn programmer greeting"
        }
    }
  5. 接下来,在同一目录中添加一个 app_es.arb 文件,对同一条信息做西班牙语的翻译:

    Next, add an app_es.arb file in the same directory for Spanish translation of the same message:

    {
        "helloWorld": "¡Hola Mundo!"
    }
  6. 要测试本地化工具,可以运行您的应用程序。您将在 ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n 中看到生成的文件。

    Now, run your app so that codegen takes place. You should see generated files in ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n.

  7. 在调用 MaterialApp 的构造函数时候,添加 import 语句,导入 app_localizations.dartAppLocalizations.delegate

    Add the import statement on app_localizations.dart and AppLocalizations.delegate in your call to the constructor for MaterialApp.

    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    return const MaterialApp(
      title: 'Localizations Sample App',
      localizationsDelegates: [
        AppLocalizations.delegate, // Add this line
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        Locale('en', ''), // English, no country code
        Locale('es', ''), // Spanish, no country code
      ],
      home: MyHomePage(),
    );
  8. 在你应用的任何地方,都使用 AppLocalizations,这里它被用于在 Text widget 里展示翻译过的消息。

    Use AppLocalizations anywhere in your app. Here, the translated message is used in a Text widget.

    Text(AppLocalizations.of(context)!.helloWorld);
  9. 您也可以使用生成的 localizationsDelegatessupportedLocales 列表,而不是手动提供它们。

    You can also use the generated localizationsDelegates and supportedLocales list instead of providing them manually.

    const MaterialApp(
      title: 'Localizations Sample App',
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
    );

    如果目标设备的语言环境设置为英语,此代码生成的 Text widget 会展示「Hello World!」。如果目标设备的语言环境设置为西班牙语,则展示「Hola Mundo!」,在 arb 文件中,每个条目的键值都被用作 getter 的方法名称,而该条目的值则表示本地化的信息。

    This code generates a Text widget that displays “Hello World!” if the target device’s locale is set to English, and “¡Hola Mundo!” if the target device’s locale is set to Spanish. In the arb files, the key of each entry is used as the method name of the getter, while the value of that entry contains the localized message.

要查看使用该工具的示例 Flutter 应用,请参阅 gen_l10n_example

To see a sample Flutter app using this tool, please see gen_l10n_example.

如需本地化设备应用描述,你可以将本地化后的字符串传递给 MaterialApp.onGenerateTitle:

To localize your device app description, you can pass in the localized string into MaterialApp.onGenerateTitle:

return MaterialApp(
  onGenerateTitle: (BuildContext context) =>
      DemoLocalizations.of(context).title,

有关本地化工具的更多信息,例如处理 DateTime 和复数,请参见 国际化用户指南

For more information about the localization tool, such as dealing with DateTime and handling plurals, see the Internationalization User’s Guide.

iOS 本地化:更新 iOS app bundle

Localizing for iOS: Updating the iOS app bundle

iOS 应用在内置于应用程序包中的 Info.plist 文件中定义了关键的应用程序元数据,其中包括了受支持的语言环境,要配置您的应用支持的语言环境,请按照以下步骤进行操作:

iOS applications define key application metadata, including supported locales, in an Info.plist file that is built into the application bundle. To configure the locales supported by your app, use the following instructions:

  1. 打开项目的 ios/Runner.xcworkspace Xcode 文件。

    Open your project’s ios/Runner.xcworkspace Xcode file.

  2. Project Navigator 中,打开 Runner 项目的 Runner 文件夹下的 Info.plist 文件。

    In the Project Navigator, open the Info.plist file under the Runner project’s Runner folder.

  3. 选择 Information Property List 项。然后从 Editor 菜单中选择 Add Item,接着从弹出菜单中选择 Localizations

    Select the Information Property List item. Then select Add Item from the Editor menu, and select Localizations from the pop-up menu.

  4. 选择并展开新创建的 Localizations 项。对于您的应用程序支持的每种语言环境,请添加一个新项,然后从 Value 字段中的弹出菜单中选择要添加的语言环境。该列表应需要与 supportedLocales 参数中列出的语言一致。

    Select and expand the newly-created Localizations item. For each locale your application supports, add a new item and select the locale you wish to add from the pop-up menu in the Value field. This list should be consistent with the languages listed in the supportedLocales parameter.

  5. 添加所有受支持的语言环境后,保存文件。

    Once all supported locales have been added, save the file.

定制的进阶操作

Advanced topics for further customization

本节介绍自定义本地 Flutter 应用程序的其他方法。

This section covers additional ways to customize a localized Flutter application.

高级语言环境定义

Advanced locale definition

一些具有着多个变体的语言仅用 languageCode 来区分是不够充分的。

Some languages with multiple variants require more than just a language code to properly differentiate.

例如,在多语言应用开发这个话题里,如果要更好的区分具有多种语言变体的中文,则需要指定其 languageCodescriptCodecountryCode。因为目前有两种主要的,且存在地区使用差异的中文书写系统:简体和繁体。

For example, fully differentiating all variants of Chinese requires specifying the language code, script code, and country code. This is due to the existence of simplified and traditional script, as well as regional differences in the way characters are written within the same script type.

为了让 CNTWHK 能够更充分地表示到每个中文变体,构建应用时,设定支持的语言列表可以参考如下代码:

In order to fully express every variant of Chinese for the country codes CN, TW, and HK, the list of supported locales should include:

supportedLocales: [
  Locale.fromSubtags(languageCode: 'zh'), // generic Chinese 'zh'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hans'), // generic simplified Chinese 'zh_Hans'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant'), // generic traditional Chinese 'zh_Hant'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hans',
      countryCode: 'CN'), // 'zh_Hans_CN'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant',
      countryCode: 'TW'), // 'zh_Hant_TW'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant',
      countryCode: 'HK'), // 'zh_Hant_HK'
],

代码里这几组明确和完整的定义,可以确保你的应用为各种不同首选语言环境的用户提供更加精细化的本地化内容。如果用户没有指定首选的语言环境,那么我们就会使用最近的匹配,这可能与用户的期望会有差异。 Flutter 只会解析定义在 supportedLocales 里面的语言环境。对于那些常用语言,Flutter 为本地化内容提供了文字代码级别的区分。查看 Localizations 了解 Flutter 是如何解析支持的语言环境和首选的语言环境的。

This explicit full definition ensures that your app can distinguish between and provide the fully nuanced localized content to all combinations of these country codes. If a user’s preferred locale is not specified, then the closest match is used instead, which likely contains differences to what the user expects. Flutter only resolves to locales defined in supportedLocales. Flutter provides scriptCode-differentiated localized content for commonly used languages. See Localizations for information on how the supported locales and the preferred locales are resolved.

虽然中文是最主要的一个示例,但是其他语言如法语(fr_FRfr_CA 等等)也应该为了更细致的本地化而做完全的区分。

Although Chinese is a primary example, other languages like French (fr_FR, fr_CA) should also be fully differentiated for more nuanced localization.

获取语言环境:Locale 类和 Localizations Widget

Tracking the locale: The Locale class and the Localizations widget

Locale 类用来识别用户的语言。移动设备支持为所有的应用设置语言环境,经常是通过系统设置菜单来进行操作。设置完之后,国际化的 app 就会展示成对应特定语言环境的值。例如,如果用户把设备的语言环境从英语切换到法语,显示 “Hello World” 的文本 widget 会使用 “Bonjour le monde” 进行重建。

The Locale class identifies the user’s language. Mobile devices support setting the locale for all applications, usually using a system settings menu. Internationalized apps respond by displaying values that are locale-specific. For example, if the user switches the device’s locale from English to French, then a Text widget that originally displayed “Hello World” would be rebuilt with “Bonjour le monde”.

Localizations widget 定义了它的子节点的语言环境和依赖的本地化的资源。 WidgetsApp 创建了一个本地化的 widget,如果系统的语言环境变化了,它会重建这个 widget。

The Localizations widget defines the locale for its child and the localized resources that the child depends on. The WidgetsApp widget creates a Localizations widget and rebuilds it if the system’s locale changes.

你可以通过调用 Localizations.localeOf() 方法来查看 app 当前的语言环境。

You can always lookup an app’s current locale with Localizations.localeOf():

Locale myLocale = Localizations.localeOf(context);

Specifying the app’s supported­Locales parameter

Although the flutter_localizations library currently supports 78 languages and language variants, only English language translations are available by default. It’s up to the developer to decide exactly which languages to support.

The MaterialApp supportedLocales parameter limits locale changes. When the user changes the locale setting on their device, the app’s Localizations widget only follows suit if the new locale is a member of this list. If an exact match for the device locale isn’t found, then the first supported locale with a matching languageCode is used. If that fails, then the first element of the supportedLocales list is used.

An app that wants to use a different “locale resolution” method can provide a localeResolutionCallback. For example, to have your app unconditionally accept whatever locale the user selects:

MaterialApp(
  localeResolutionCallback: (
    Locale? locale,
    Iterable<Locale> supportedLocales,
  ) {
    return locale;
  },
);

Flutter 里的国际化是如何工作的

How internationalization in Flutter works

本节涵盖了 Flutter 中本地化工作的技术细节,如果你计划使用自定的一套本地化消息,下面的内容会很有帮助。反之则可以跳过本节。

This section covers the technical details of how localizations work in Flutter. If you’re planning on supporting your own set of localized messages, the following content would be helpful. Otherwise, you can skip this section.

加载和获取本地化值

Loading and retrieving localized values

我们使用 Localizations widget 来加载和查询那些包含本地化值集合的对象。 app 通过调用 Localizations.of(context,type) 来引用这些对象。如果设备的语言环境变化了,Localizations widget 会自动地加载新的语言环境的值,然后重建那些使用了语言环境的 widget。这是因为 Localizations继承 widget 一样执行。当一个构建过程涉及到继承 widget,对继承 widget 的隐式依赖就创建了。当一个继承 widget 变化了(即 Localizations widget 的语言环境变化),它的依赖上下文就会被重建。

The Localizations widget is used to load and lookup objects that contain collections of localized values. Apps refer to these objects with Localizations.of(context,type). If the device’s locale changes, the Localizations widget automatically loads values for the new locale and then rebuilds widgets that used it. This happens because Localizations works like an InheritedWidget. When a build function refers to an inherited widget, an implicit dependency on the inherited widget is created. When an inherited widget changes (when the Localizations widget’s locale changes), its dependent contexts are rebuilt.

本地化的值是通过使用 Localizations widget 的 LocalizationsDelegate 加载的。每一个 delegate 必须定义一个异步的 load() 方法。这个方法生成了一个封装本地化值的对象,通常这些对象为每个本地化的值定义了一个方法。

Localized values are loaded by the Localizations widget’s list of LocalizationsDelegates. Each delegate must define an asynchronous load() method that produces an object that encapsulates a collection of localized values. Typically these objects define one method per localized value.

在一个大型的 app 中,不同的模块或者 package 需要和它们对应的本地化资源打包在一起。这就是为什么 Localizations widget 管理着对象的一个对应表,每个 LocalizationsDelegate 对应一个对象。为了获得由 LocalizationsDelegateload 方法生成的对象,你需要指定一个构建上下文 (BuildContext) 和对象的类型。

In a large app, different modules or packages might be bundled with their own localizations. That’s why the Localizations widget manages a table of objects, one per LocalizationsDelegate. To retrieve the object produced by one of the LocalizationsDelegate’s load methods, you specify a BuildContext and the object’s type.

例如,Material 组件 widget 的本地化字符串是由 MaterialLocalizations 类定义的。这个类的实例是由 [MaterialApp 类提供的一个 LocalizationDelegate 方法创建的,它们可以通过 Localizations.of 方法获得。

For example, the localized strings for the Material Components widgets are defined by the MaterialLocalizations class. Instances of this class are created by a LocalizationDelegate provided by the MaterialApp class. They can be retrieved with Localizations.of():

Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);

因为这个特定的 Localizations.of() 表达式经常使用,所以 MaterialLocalizations 类提供了一个快捷访问:

This particular Localizations.of() expression is used frequently, so the MaterialLocalizations class provides a convenient shorthand:

static MaterialLocalizations of(BuildContext context) {
  return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}

/// References to the localized values defined by MaterialLocalizations
/// are typically written like this:

tooltip: MaterialLocalizations.of(context).backButtonTooltip,

为 app 的本地化资源定义一个类

Defining a class for the app’s localized resources

综合所有这些在一起,一个需要国际化的 app 经常以一个封装 app 本地化值的类开始的。下面是使用这种类的典型示例。

Putting together an internationalized Flutter app usually starts with the class that encapsulates the app’s localized values. The example that follows is typical of such classes.

此示例 app 的 完整的源码

Complete source code for the intl_example for this app.

这个示例是基于 intl package 提供的 API 和工具开发的, app 本地化资源的替代方法 里面讲解了一个不依赖于 intl package 的 示例

This example is based on the APIs and tools provided by the intl package. An alternative class for the app’s localized resources describes an example that doesn’t depend on the intl package.

DemoLocalizations 类包含了 app 语言环境内支持的已经翻译成了本地化语言的字符串(本例子只有一个)。它通过调用由 Dart 的 intl package 生成的 initializeMessages() 方法来加载翻译好的字符串,然后使用 Intl.message() 来查阅它们。

The DemoLocalizations class defined below contains the app’s strings (just one for the example) translated into the locales that the app supports. It uses the initializeMessages() function generated by Dart’s intl package, Intl.message(), to look them up.

class DemoLocalizations {
  DemoLocalizations(this.localeName);

  static Future<DemoLocalizations> load(Locale locale) {
    final String name =
        locale.countryCode == null || locale.countryCode!.isEmpty
            ? locale.languageCode
            : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);

    return initializeMessages(localeName).then((_) {
      return DemoLocalizations(localeName);
    });
  }

  static DemoLocalizations of(BuildContext context) {
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
  }

  final String localeName;

  String get title {
    return Intl.message(
      'Hello World',
      name: 'title',
      desc: 'Title for the Demo application',
      locale: localeName,
    );
  }
}

基于 intl package 的类引入了一个生成好的信息目录,它提供了 initializeMessage() 方法和 Intl.message() 方法的每个语言环境的备份存储。 intl 工具 通过分析包含 Intl.message() 调用类的源码生成这个信息目录。在当前情况下,就是 DemoLocalizations 的类(包含了 Intl.message() 调用)。

A class based on the intl package imports a generated message catalog that provides the initializeMessages() function and the per-locale backing store for Intl.message(). The message catalog is produced by an intl tool that analyzes the source code for classes that contain Intl.message() calls. In this case that would just be the DemoLocalizations class.

添加支持新的语言

Adding support for a new language

如果你要开发一个 app 需要支持的语言不在 GlobalMaterialLocalizations 当中,那就需要做一些额外的工作:它必须提供大概 70 个字和词还有日期以及符号的翻译(本地化)。

An app that needs to support a language that’s not included in GlobalMaterialLocalizations has to do some extra work: it must provide about 70 translations (“localizations”) for words or phrases and the date patterns and symbols for the locale.

举个例子,我们将给大家展示如何支持挪威尼诺斯克语。

See the following for an example of how to add support for the Norwegian Nynorsk language.

我们需要定义一个新的 GlobalMaterialLocalizations 子类,它定义了 Material 库依赖的本地化资源。同时,我们也必须定义一个新的 LocalizationsDelegate 子类,它是给 GlobalMaterialLocalizations 子类作为一个工厂使用的。

A new GlobalMaterialLocalizations subclass defines the localizations that the Material library depends on. A new LocalizationsDelegate subclass, which serves as factory for the GlobalMaterialLocalizations subclass, must also be defined.

这是支持添加一种新语言的一个完整例子的源码,相对实际上要翻译的尼诺斯克语数量,我们只翻译了一小部分。

Here’s the source code for the complete add_language example, minus the actual Nynorsk translations.

这个特定语言环境的 GlobalMaterialLocalizations 子类被称为 NnMaterialLocalizationsLocalizationsDelegate 子类被称为 _NnMaterialLocalizationsDelegateBeMaterialLocalizations.delegate 是 delegate 的一个实例,这就是 app 使用这些本地化所需要的全部。

The locale-specific GlobalMaterialLocalizations subclass is called NnMaterialLocalizations, and the LocalizationsDelegate subclass is _NnMaterialLocalizationsDelegate. The value of NnMaterialLocalizations.delegate is an instance of the delegate, and is all that’s needed by an app that uses these localizations.

delegate 类包括基本的日期和数字格式的本地化。其他所有的本地化是由 BeMaterialLocalizations 里面的 String 字符串值属性的 getters 所定义的,像下面这样:

The delegate class includes basic date and number format localizations. All of the other localizations are defined by String valued property getters in NnMaterialLocalizations, like this:

@override
String get moreButtonTooltip => r'More';

@override
String get aboutListTileTitleRaw => r'About $applicationName';

@override
String get alertDialogLabel => r'Alert';

当然,这些都是英语翻译。为了完成本地化操作,你需要把每一个 getter 的返回值翻译成合适的新挪威语 (Nynorsk) 字符串。

These are the English translations, of course. To complete the job you need to change the return value of each getter to an appropriate Nynorsk string.

r'About $applicationName' 一样,这些带 r 前缀的 getters 返回的是原始的字符串,因为有一些时候这些字符串会包含一些带有 $ 前缀的变量。通过调用带参数的本地化方法,这些变量会被替换:

The getters return “raw” Dart strings that have an r prefix, like r'About $applicationName', because sometimes the strings contain variables with a $ prefix. The variables are expanded by parameterized localization methods:

@override
String get pageRowsInfoTitleRaw => r'$firstRow–$lastRow of $rowCount';

@override
String get pageRowsInfoTitleApproximateRaw =>
    r'$firstRow–$lastRow of about $rowCount';

语言对应的日期格式和符号需要一并指定。在源码中,它们会以下列形式进行定义:

The date patterns and symbols of the locale will also need to be specified. In the source code, the date patterns and symbols are defined like this:

const nnLocaleDatePatterns = {
  'd': 'd.',
  'E': 'ccc',
  'EEEE': 'cccc',
  'LLL': 'LLL',
  // ...
}
const nnDateSymbols = {
  'NAME': 'nn',
  'ERAS': <dynamic>[
    'f.Kr.',
    'e.Kr.',
  ],
  // ...
}

上列内容需要修改以匹配语言的正确日期格式。可惜的是,intl 并不具备数字格式的灵活性,以至于 _NnMaterialLocalizationsDelegate 需要使用现有的语言的格式作为替代方法:

These will need to be modified for the locale to use the correct date formatting. Unfortunately, since the intl library does not share the same flexibility for number formatting, the formatting for an existing locale will have to be used as a substitute in _NnMaterialLocalizationsDelegate:

class _NnMaterialLocalizationsDelegate
    extends LocalizationsDelegate<MaterialLocalizations> {
  const _NnMaterialLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'nn';

  @override
  Future<MaterialLocalizations> load(Locale locale) async {
    final String localeName = intl.Intl.canonicalizedLocale(locale.toString());

    // The locale (in this case `nn`) needs to be initialized into the custom
    // date symbols and patterns setup that Flutter uses.
    date_symbol_data_custom.initializeDateFormattingCustom(
      locale: localeName,
      patterns: nnLocaleDatePatterns,
      symbols: intl.DateSymbols.deserializeFromMap(nnDateSymbols),
    );

    return SynchronousFuture<MaterialLocalizations>(
      NnMaterialLocalizations(
        localeName: localeName,
        // The `intl` library's NumberFormat class is generated from CLDR data
        // (see https://github.com/dart-lang/intl/blob/master/lib/number_symbols_data.dart).
        // Unfortunately, there is no way to use a locale that isn't defined in
        // this map and the only way to work around this is to use a listed
        // locale's NumberFormat symbols. So, here we use the number formats
        // for 'en_US' instead.
        decimalFormat: intl.NumberFormat('#,##0.###', 'en_US'),
        twoDigitZeroPaddedFormat: intl.NumberFormat('00', 'en_US'),
        // DateFormat here will use the symbols and patterns provided in the
        // `date_symbol_data_custom.initializeDateFormattingCustom` call above.
        // However, an alternative is to simply use a supported locale's
        // DateFormat symbols, similar to NumberFormat above.
        fullYearFormat: intl.DateFormat('y', localeName),
        compactDateFormat: intl.DateFormat('yMd', localeName),
        shortDateFormat: intl.DateFormat('yMMMd', localeName),
        mediumDateFormat: intl.DateFormat('EEE, MMM d', localeName),
        longDateFormat: intl.DateFormat('EEEE, MMMM d, y', localeName),
        yearMonthFormat: intl.DateFormat('MMMM y', localeName),
        shortMonthDayFormat: intl.DateFormat('MMM d'),
      ),
    );
  }

  @override
  bool shouldReload(_NnMaterialLocalizationsDelegate old) => false;
}

需要了解更多关于本地化字符串的内容,可以查看 flutter_localizations README

For more information about localization strings, see the flutter_localizations README.

一旦你实现了指定语言的 GlobalMaterialLocalizationsLocalizationsDelegate 的子类,你只需要给你的 app 添加此语言以及一个 delegate 的实例。这里有一些代码展示了如何设置 app 的语言为尼诺斯克语以及如何给 app 的 localizationsDelegates 列表添加 NnMaterialLocalizations delegate 实例。

Once you’ve implemented your language-specific subclasses of GlobalMaterialLocalizations and LocalizationsDelegate, you just need to add the language and a delegate instance to your app. Here’s some code that sets the app’s language to Nynorsk and adds the NnMaterialLocalizations delegate instance to the app’s localizationsDelegates list:

const MaterialApp(
  localizationsDelegates: [
    GlobalWidgetsLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    NnMaterialLocalizations.delegate, // Add the newly created delegate
  ],
  supportedLocales: [
    Locale('en', 'US'),
    Locale('nn'),
  ],
  home: Home(),
),

其他的国际化方法

Alternative internationalization workflows

本节主要介绍国际化 Flutter 应用的不同方法。

This section describes different approaches to internationalize your Flutter application.

应用程序本地化资源的替代类

An alternative class for the app’s localized resources

之前的示例应用主要根据 Dart intl package 定义,为了简单起见,或者可能想要与不同的 i18n 框架集成,开发者也可以选择自己的方法来管理本地化的值。

The previous example was defined in terms of the Dart intl package. Developers can choose their own approach for managing localized values for the sake of simplicity or perhaps to integrate with a different i18n framework.

点击查看 minimal 应用的完整源代码。

Complete source code for the minimal app.

在下面这个样例中,包含应用本地化版本的类 DemoLocalizations 直接在每种语言的 Map 中包括了所有的翻译。

In the below example, the DemoLocalizations class includes all of its translations directly in per language Maps.

class DemoLocalizations {
  DemoLocalizations(this.locale);

  final Locale locale;

  static DemoLocalizations of(BuildContext context) {
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
  }

  static const _localizedValues = <String, Map<String, String>>{
    'en': {
      'title': 'Hello World',
    },
    'es': {
      'title': 'Hola Mundo',
    },
  };

  static List<String> languages ()=> _localizedValues.keys.toList();

  String get title {
    return _localizedValues[locale.languageCode]!['title']!;
  }
}

在 minimal 应用中,DemoLocalizationsDelegate 略有不同,它的 load 方法返回一个 SynchronousFuture,因为不需要进行异步加载。

In the minimal app the DemoLocalizationsDelegate is slightly different. Its load method returns a SynchronousFuture because no asynchronous loading needs to take place.

class DemoLocalizationsDelegate
    extends LocalizationsDelegate<DemoLocalizations> {
  const DemoLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => DemoLocalizations.languages().contains(locale.languageCode);


  @override
  Future<DemoLocalizations> load(Locale locale) {
    // Returning a SynchronousFuture here because an async "load" operation
    // isn't needed to produce an instance of DemoLocalizations.
    return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
  }

  @override
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}

附录:使用 Dart intl 工具

Using the Dart intl tools

在你使用 Dart intl package 进行构建 API 之前,你应该想要了解一下 intl package 的文档。

Before building an API using the Dart intl package you’ll want to review the intl package’s documentation. Here’s a summary of the process for localizing an app that depends on the intl package.

这个 demo app 依赖于一个生成的源文件,叫做 l10n/messages_all.dart,这个文件定义了 app 使用的所有本地化的字符串。

The demo app depends on a generated source file called l10n/messages_all.dart, which defines all of the localizable strings used by the app.

重建 l10n/messages_all.dart 需要 2 步。

Rebuilding l10n/messages_all.dart requires two steps.

  1. 在 app 的根目录,使用 lib/main.dart 生成 l10n/intl_messages.arb

    With the app’s root directory as the current directory, generate l10n/intl_messages.arb from lib/main.dart:

    $ flutter pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/main.dart
    

    intl_messages.arb 是一个 JSON 格式的文件,每一个入口代表定义在 main.dart 里面的 Intl.message() 方法。 intl_en.arbintl_es.arb 分别作为英语和西班牙语翻译的模板。这些翻译是由你(开发者)来创建的。

    The intl_messages.arb file is a JSON format map with one entry for each Intl.message() function defined in main.dart. This file serves as a template for the English and Spanish translations, intl_en.arb and intl_es.arb. These translations are created by you, the developer.

  2. 在 app 的根目录,生成每个 intl_<locale>.arb 文件对应的 intl_messages_<locale>.dart 文件,以及 intl_messages_all.dart 文件,它引入了所有的信息文件。

    With the app’s root directory as the current directory, generate intl_messages_<locale>.dart for each intl_<locale>.arb file and intl_messages_all.dart, which imports all of the messages files:

    $ flutter pub run intl_translation:generate_from_arb \
        --output-dir=lib/l10n --no-use-deferred-loading \
        lib/main.dart lib/l10n/intl_*.arb
    

    Windows 系统不支持文件名通配符。列出的 .arb 文件是由 intl_translation:extract_to_arb 命令生成的。

    Windows does not support file name wildcarding. Instead, list the .arb files that were generated by the intl_translation:extract_to_arb command.

    $ flutter pub run intl_translation:generate_from_arb \
        --output-dir=lib/l10n --no-use-deferred-loading \
        lib/main.dart \
        lib/l10n/intl_en.arb lib/l10n/intl_fr.arb lib/l10n/intl_messages.arb
    

    DemoLocalizations 类使用生成的 initializeMessages() 方法(该方法定义在 intl_messages_all.dart 文件)来加载本地化的信息,然后使用 Intl.message() 来查阅这些本地化的信息。

    The DemoLocalizations class uses the generated initializeMessages() function (defined in intl_messages_all.dart) to load the localized messages and Intl.message() to look them up.

已支持的平台

目录

已支持的平台

Supported platforms

2.5 版本之后,Flutter 支持以下平台:

As of release 2.5, Flutter supports the following platforms:

平台

Platform

版本

Version

渠道

Channels

Android

API 19 及以上

API 19 & above

全部

All

iOS

iOS 9 及以上

iOS 9 & above

全部

All

Linux

Debian 10 及以上

Debian 10 & above

全部

All

macOS

El Capitan 及以上

El Capitan & above

全部

All

Web

Chrome 84 及以上

Chrome 84 & above

全部

All

Web

Firefox 72.0 及以上

Firefox 72.0 & above

全部

All

Web

Safari on El Capitan 及以上

Safari on El Capitan & above

全部

All

Web

Edge 1.2.0 及以上

Edge 1.2.0 & above

全部

All

Windows

Windows 7 及以上

Windows 7 & above

全部

All

我们如何定义一个已支持的平台

How we define a supported platform

从 Flutter 1.20 开始,我们为 Flutter 运行的平台定义了三层支撑:

As of Flutter 1.20, we define three tiers of support for the platforms on which Flutter runs:

  1. 支持谷歌测试平台,这些平台是谷歌 Flutter 团队在每次提交时集成测试的平台。对于这些平台,在从 master 分支合并到 dev 通道之前,我们还会运行提交测试。

    Supported Google-tested platforms, which are platforms the Flutter team at Google tests in continuous integration at every commit. For these platforms, we also run post-commit tests before rolling from the master channel to the dev channel.

  2. 由社区测试尽力支持的平台,我们相信这些平台是通过编码实践和特别测试支持的,但依赖社区进行测试。

    Best effort platforms, supported community testing, which are platforms we believe we support through coding practices and ad-hoc testing, but rely on the community for testing.

  3. 不受支持的平台,这些平台可以工作,但开发团队不直接测试或支持。

    Unsupported platforms, which are platforms that may work, but that the development team does not directly test or support.

支持谷歌测试的平台

Supported Google-tested platforms

平台

Platform

版本

Version

Android Android SDK 30
Android Android SDK 29
Android Android SDK 28
Android Android SDK 27
Android Android SDK 26
Android Android SDK 25
Android Android SDK 24
Android Android SDK 23
Android Android SDK 22
Android Android SDK 21
Android Android SDK 19
iOS 14 (all)
iOS 13.3-13.7
iOS 13.0
iOS 12.4 & 12.4.1
iOS 9.3.6
Web Chrome 84
Web Firefox 72.0
Web Safari / Catalina
Web Edge 1.2.0
Windows Windows 10
macOS El Capitan & greater
Linux Debian 10

请注意,Android SDK 19 的测试涵盖 Android SDK 20,因为两个版本之间的差异很小。

Note that Android SDK 20 is covered by testing Android SDK 19, as the differences between the two platform versions are minimal.

社区测试尽力支持的平台

Best effort platforms tested by the community

平台

Platform

版本

Version

Android Android SDK 22
Android Android SDK 20
Android Android SDK 19
Android Android SDK 18
Android Android SDK 17
Android Android SDK 16
iOS iOS 13.1
iOS iOS 12.1-12.3
iOS iOS 10 (all)
iOS iOS 9.0
Windows Windows 8
Windows Windows 7
Linux Debian & below

我们已经放弃对 iOS 8 的支持。更多信息,请参阅 go/rfc-ios8-deprecation

We have dropped iOS8 support. For more information, see go/rfc-ios8-deprecation for details.

不受支持的平台

Unsupported platforms

平台

Platform

版本

Version

Android

Android SDK 15 及以上

Android SDK 15 & below

iOS

iOS 8 及以上

iOS 8 & below

Windows

Windows Vista 及以上

Windows Vista & below

Windows

任何 32 位平台

Any 32-bit platform

macOS

Yosemite 及以上

Yosemite & below

添加 iOS App Clip target

目录

这个指南介绍了如何手动添加另一个使用 Flutter 来渲染的 iOS App Clip target, 并将它集成到您现有的 Flutter 项目或 add-to-app 项目。

This guide describes how to manually add another Flutter-rendering iOS App Clip target to your existing Flutter project or add-to-app project.

如果您有兴趣将 App Clip 自动集成到 iOS 应用中,请参阅功能请求 #65451.

If you are interested in automatically integrating an App Clip into your iOS app, see feature request #65451.

要查看完整可用的示例,请参阅 GitHub 上的 App Clip 示例

To see a working sample, see the App Clip sample on GitHub.

步骤 1 - 打开项目

Step 1 - Open project

打开您的 iOS Xcode 工程,例如您的纯 Flutter 项目中的 ios/Runner.xcworkspace

Open your iOS Xcode project, such as ios/Runner.xcworkspace for full-Flutter apps.

步骤 2 - 添加一个 App Clip 的 target

Step 2 - Add an App Clip target

2.1

点击您项目的 Project Navigator 来显示工程设置。

Click on your project in the Project Navigator to show the project settings.

点击 target 列表底部的 + 来添加一个新的 target。

Press + at the bottom of the target list to add a new target.

2.2

为您的新 target 选择 App Clip 类型。

Select the App Clip type for your new target.

2.3

在对话框为您的新 target 输入详情。

Enter your new target detail in the dialog.

选择 Storyboard 作为界面。

Select Storyboard for Interface.

选择 UIKit App Delegate 作为生命周期。

Select UIKit App Delegate for Life Cycle.

选择与您原来的 target 相同的编程语言。

Select the same language as your original target for Language.

(换句话说,请勿为 Objective-C target 创建 Swift 类型的 App Clip target,反之亦然,以简化设置。)

(In other words, don’t create a Swift App Clip target for an Objective-C main target, and vice versa to simplify the setup.)

2.4

在接下来的对话框中,为新的 target 激活 (activate) 一个新的 scheme。

In the following dialog, activate the new scheme for the new target.

步骤 3 - 移除不需要的文件

Step 3 - Remove unneeded files

3.1

在项目 Project Navigator 的新创建的 App Clip 组中,将除了 Info.plistApp Clip target.entitlements 以外的所有内容删除。

In the Project Navigator, in the newly created App Clip group, delete everything except Info.plist and <app clip target>.entitlements.

移动文件到废纸篓。

Move files to trash.

3.2

如果您不使用 SceneDelegate.swift 文件,移除在 Info.plist 中对应的引用。

If you don’t use the SceneDelegate.swift file, remove the reference to it in the Info.plist.

打开 App Clip 组中的 Info.plist。删除 Application Scene Manifest 字典条目。

Open the Info.plist file in the App Clip group. Delete the entire dictionary entry for Application Scene Manifest.

步骤 4 - 共享构建配置

Step 4 - Share build configurations

对于 add-to-app 项目,此步骤不是必需的,因为 add-to-app 有自己的自定义构建配置和版本。

This step isn’t necessary for add-to-app projects since add-to-app projects have their custom build configurations and versions.

4.1

返回项目设置,现在选择 Project 条目,而不是 Targets 里的任何 target。

Back in the project settings, select the project entry now rather than any targets.

Info 选项卡页中的 Configurations 可扩展组下,展开 DebugProfileRelease 条目。

In the Info tab, under the Configurations expandable group, expand the Debug, Profile, and Release entries.

每一个 App Clip target 的下拉菜单的值都应该与常规应用 target 中的值相同。

For each, select the same value from the drop-down menu for the App Clip target as the entry selected for the normal app target.

这使您的 App Clip target 可以访问 Flutter 的必需构建设置。

This gives your App Clip target access to Flutter’s required build settings.

4.2

在 App Clip 组的 Info.plist 文件中,设置:

In the App Clip group’s Info.plist file, set:

步骤 5 - 共享代码和资源

Step 5 - Share code and assets

选项 1 - 共享所有东西

Option 1 - Share everything

假设您的目标是在 App Clip 中显示与普通应用相同的 Flutter UI,并共享相同的代码和资源。

Assuming the intent is to show the same Flutter UI in the standard app as in the App Clip, share the same code and assets.

对于以下每一个文件: Main.storyboardAssets.xcassetsLaunchScreen.storyboardGeneratedPluginRegistrant.mAppDelegate.swift,(如果您是 Objective-C 还应该包括 Supporting Files/main.m)选择文件并且在检查器中选择第一个选项卡,并且在 Target Membership 选中 App Clip

For each of the following: Main.storyboard, Assets.xcassets, LaunchScreen.storyboard, GeneratedPluginRegistrant.m, and AppDelegate.swift (and Supporting Files/main.m if using Objective-C), select the file, then in the first tab of the inspector, also include the App Clip target in the Target Membership checkbox group.

选项 2 - 为 App Clip 自定义 Flutter 的启动器

Option 2 - Customize Flutter launch for App Clip

在这个例子中,不需要删除在 步骤 3 中的任何东西。相对的,使用 iOS add-to-app APIs 的模板来自定义 Flutter 启动器。可以参考示例 自定义 Flutter 路由

In this case, do not delete everything listed in Step 3. Instead, use the scaffolding and the iOS add-to-app APIs to perform a custom launch of Flutter. For example to show a custom Flutter route.

步骤 6 - 添加 App Clip 的关联域名

Step 6 - Add App Clip associated domains

这是一个 App Clip 开发的标准步骤。请查看 苹果官方文档

This is a standard step for App Clip development. See the official Apple documentation.

6.1

打开 <app clip target>.entitlements 文件。添加 Associated Domains 数组。添加一行 appclips:<your bundle id> 到数组中。

Open the <app clip target>.entitlements file. Add an Associated Domains Array type. Add a row to the array with appclips:<your bundle id>.

6.2

同样的相关域名权利也需要添加到您的主应用程序中。

The same associated domains entitlement needs to be added to your main app as well.

<app clip target>.entitlements 文件从 App Clip 组复制到主应用程序组,并将其重命名为与主目标相同的名称,例如 Runner.entitlements

Copy the <app clip target>.entitlements file from your App Clip group to your main app group and rename it to the same name as your main target such as Runner.entitlements.

打开文件并删除主应用程序授权文件的 Parent Application Identifiers 条目(将该条目保留为 App Clip 的授权文件)。

Open the file and delete the Parent Application Identifiers entry for the main app’s entitlement file (leave that entry for the App Clip’s entitlement file).

6.3

返回项目设置,选择主应用 target,打开 Build Settings 选项卡。设置 Code Signing Entitlements 的值为主应用创建的第二个授权文件的相对路径。

Back in the project settings, select the main app’s target, open the Build Settings tab. Set the Code Signing Entitlements setting to the relative path of the second entitlements file created for the main app.

步骤 7 - 整合 Flutter

Step 7 - Integrate Flutter

add-to-app 不需要这些步骤。

These steps are not necessary for add-to-app.

7.1

在您的 App Clip 的 target 的项目设置,打开 Build Settings 选项卡。

In your App Clip’s target’s project settings, open the Build Settings tab.

Framework Search Paths 添加两个内容:

For setting Framework Search Paths, add 2 entries:

换句话说,与主应用程序 target 的构建设置相同。

In other words, the same as the main app target’s build settings.

7.2

如果是 Swift target,设置 Objective-C Bridging Header 构建配置为 Runner/Runner-Bridging-Header.h

For Swift target, set the Objective-C Bridging Header build setting to Runner/Runner-Bridging-Header.h

换句话说,与主应用程序 target 的构建设置相同。

In other words, the same as the main app target’s build settings.

7.3

现在打开 Build Phases 选项卡。点击 + 并且选择 New Run Script Phase

Now open the Build Phases tab. Press the + sign and select New Run Script Phase.

拖动新的 phase 到 Dependencies phase。

Drag that new phase to below the Dependencies phase.

展开新 phase 并将以下内容添加到脚本:

Expand the new phase and add this line to the script content:

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

简单来说,与主应用程序 target 的构建设置相同。

In other words, the same as the main app target’s build phases.

这可以确保在运行 App Clip target 时编译 Flutter 的 Dart 代码。

This ensures that your Flutter Dart code is compiled when running the App Clip target.

7.4

再次点击 + 并且选择 New Run Script Phase。这是最后一个 phase。

Press the + sign and select New Run Script Phase again. Leave it as the last phase.

这次添加如下内容:

This time, add:

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed_and_thin

简单来说,与主应用程序 target 的构建设置相同。

In other words, the same as the main app target’s build phases.

这将确保您的 Flutter 应用程序和引擎嵌入到 App Clip bundle 中。

This ensures that your Flutter app and engine are embedded into the App Clip bundle.

Step 8 - Disable Bitcode

在 App Clip target 的 Build Settings 选项卡中,将 Enable Bitcode 设置设置为 No。

In the App Clip target’s Build Settings tab, set the Enable Bitcode setting to No.

Step 9 - 整合插件

Step 9 - Integrate plugins

9.1

在您的 Flutter 项目或是 add-to-app 的宿主项目中打开 Podfile 文件。

Open the Podfile for your Flutter project or add-to-app host project.

如果是完整的 Flutter 项目,替换下面这段代码:

For full-Flutter apps, replace the following section:

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

为:

with:

use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))

target 'Runner'
target '<name of your App Clip target>'

在文件的开始,需要把 platform :ios, '9.0' 的注释解除,并且为您的 2 个 target 设置最低可运行的 iOS 系统版本。

At the top of the file, also uncomment platform :ios, '9.0' and set the version to the lowest of the 2 target’s iOS Deployment Target.

如果是 add-to-app,紧跟下面的代码:

For add-to-app, add to:

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

添加:

with:

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

target '<name of your App Clip target>'
  install_all_flutter_pods(flutter_application_path)
end

9.2

在命令行中,目前工作目录需要是您的 Flutter 项目目录。

From the command line, enter your Flutter project directory.

cd ios

然后运行

then

pod install.

运行

Run

您现在可以在 Xcode 的 scheme 下拉中选择并运行您的 App Clip target 了,选择一个 iOS 14 的设备并点击运行。

You can now run your App Clip target from Xcode by selecting your App Clip target from the scheme drop-down, selecting an iOS 14 device and pressing run.

要从头测试 App Clip 的启动,您也可以查看苹果公司的文档 测试您的 App Clip 的启动体验

To test launching an App Clip from the beginning, also consult Apple’s doc on Testing Your App Clip’s Launch Experience.

调试和热重载

Debugging, hot reload

不幸的是,由于网络权限的原因,flutter attach 无法在 App clip 中自动发现 Flutter 会话。

Unfortunately flutter attach cannot auto-discover the Flutter session in an App Clip due to networking permission restrictions.

为了调试 App clip 并使用诸如热重新加载之类的功能,必须在运行应用后从 Xcode 中的控制台输出中查找 Observatory URI。

In order to debug your App Clip and use functionalities like hot reload, you must look for the Observatory URI from the console output in Xcode after running.

您需要复制粘贴它们到 flutter attach 来连接。

You must then copy paste it back into the flutter attach command to connect.

例如:

For example:

flutter attach --debug-uri <copied URI>

作为 Apple Watch 插件

目录

虽然您不能使用 Flutter 构建 Apple Watch 应用程序,但可以向 Flutter 应用程序添加 Apple Watch 的本地扩展。

While you cannot build an Apple Watch app with Flutter, it is possible to add a native Apple Watch extension to a Flutter app.

步骤 1: 在 Xcode 中开启 bitcode

Step 1: Enable bitcode in Xcode

Apple Watch target 要求启用 bitcode,因此请按照 创建支持 bitcode 的 iOS 应用程序 中的步骤在您的应用程序中使用 bitcode。

Apple Watch targets require bitcode to be enabled, so follow the steps in Creating an iOS Bitcode enabled app to use bitcode in your app.

步骤 2: 添加一个 Apple Watch target

Step 2: Add an Apple Watch target

在菜单中,选择 File > New > Target。当对话框打开后,在顶部选择 watchOS 并且点击 Watch App for iOS App。然后,点击 Next,输入产品名,最后选择 Enter

In the menu, select File > New > Target. Once the dialog opens, select watchOS at the top and click Watch App for iOS App. Click Next, enter a product name, and select Enter.

Adding an Apple Watch target

使用 dart:ffi 调用本地代码

目录

Flutter 移动版可以使用 dart:ffi 库来调用本地的 C API。 FFI 代表 外部功能接口。类似功能的其他术语包括本地接口语言绑定

Flutter mobile can use the dart:ffi library to call native C APIs. FFI stands for foreign function interface. Other terms for similar functionality include native interface and language bindings.

您必须首先确保本地代码已加载,并且其符号对 Dart 可见,然后才能在库或程序使用 FFI 库绑定本地代码。本页主要介绍如何在 Flutter 插件或应用程序中编译、打包和加载本地代码。

Before your library or program can use the FFI library to bind to native code, you must ensure that the native code is loaded and its symbols are visible to Dart. This page focuses on compiling, packaging, and loading native code within a Flutter plugin or app.

本教程演示了如何在 Flutter 插件中捆绑 C/C++ 源代码,并使用 Android 和 iOS 上的 Dart FFI 库绑定它们。在本示例中,您将创建一个实现 32 位的加法 C 函数,然后通过名为 “native_add” 的 Dart 插件暴露它。

This tutorial demonstrates how to bundle C/C++ sources in a Flutter plugin and bind to them using the Dart FFI library on both Android and iOS. In this walkthrough, you’ll create a C function that implements 32-bit addition and then exposes it through a Dart plugin named “native_add”.

动态链接 vs 静态链接

Dynamic vs static linking

本地库可以动态或静态地链接到应用程序中。一个静态链接库会被嵌入到应用程序的可执行映像中,并在应用程序启动时加载。

A native library can be linked into an app either dynamically or statically. A statically linked library is embedded into the app’s executable image, and is loaded when the app starts.

静态链接中的符号可以使用 DynamicLibrary.executableDynamicLibrary.process 来加载.

Symbols from a statically linked library can be loaded using DynamicLibrary.executable or DynamicLibrary.process.

相比之下,动态链接库则分布在应用程序中的单独的文件或文件夹中,并按需加载。在 Android 上,动态链接库作为一组 .so(ELF 可执行与可链接格式)文件分发,每个架构各有一个。在 iOS 上,它是作为 .framework 文件夹分发的。

A dynamically linked library, by contrast, is distributed in a separate file or folder within the app, and loaded on-demand. On Android, a dynamically linked library is distributed as a set of .so (ELF) files, one for each architecture. On iOS, it’s distributed as a .framework folder.

动态链接库在 Dart 中可以通过 DynamicLibrary.open 加载。

A dynamically linked library can be loaded into Dart via DynamicLibrary.open.

Dart dev 频道中的 API 已经可用: Dart API 参考文档.

API documentation is available from the Dart dev channel: Dart API reference documentation.

步骤 1:创建插件

Step 1: Create a plugin

如果您已经有一个插件,跳过这步。

If you already have a plugin, skip this step.

如果要创建一个名为 “native_add” 的插件,您需要这么做:

To create a plugin called “native_add”, do the following:

$ flutter create --platforms=android,ios --template=plugin native_add
$ cd native_add

步骤 2:添加 C/C++ 源码

Step 2: Add C/C++ sources

您需要让 Android 和 iOS 构建系统知道本地代码的存在,以便代码可以被编译并链接到最终的应用程序中。

You need to inform both the Android and iOS build systems about the native code so the code can be compiled and linked appropriately into the final application.

您可以将源代码添加到 ios 文件夹,因为 CocoaPods 不允许源码处于比 podspec 文件更高的目录层级,但是 Gradle 允许您指向 ios 文件夹。 iOS 和 Android 不需要使用相同的源代码;当然,您也可以将特定于 Android 的源代码添加到 android 文件夹并修改 CMakeLists.txt 文件。

You add the sources to the ios folder, because CocoaPods doesn’t allow including sources above the podspec file, but Gradle allows you to point to the ios folder. It’s not required to use the same sources for both iOS and Android; you may, of course, add Android-specific sources to the android folder and modify CMakeLists.txt appropriately.

FFI 库只能与 C 符号绑定,因此在 C++ 中,这些符号添加 extern C 标记。还应该添加属性来表明符号是需要被 Dart 引用的,以防止链接器在优化链接时会丢弃符号。

The FFI library can only bind against C symbols, so in C++ these symbols must be marked extern C. You should also add attributes to indicate that the symbols are referenced from Dart, to prevent the linker from discarding the symbols during link-time optimization.

作为示例,创建一个 C++ 文件,路径为:ios/Classes/native_add.cpp。(请注意,模板已经为您创建了此文件。)在项目的根目录下中执行以下命令:

For example, to create a C++ file named ios/Classes/native_add.cpp, use the following instructions. (Note that the template has already created this file for you.) Start from the root directory of your project:

cat > ios/Classes/native_add.cpp << EOF
#include <stdint.h>

extern "C" __attribute__((visibility("default"))) __attribute__((used))
int32_t native_add(int32_t x, int32_t y) {
    return x + y;
}
EOF

在 iOS 中,您需要告诉 Xcode 如何静态链接这个文件:

On iOS, you need to tell Xcode to statically link the file:

  1. 在 Xcode 中,打开 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace.

  2. 添加 C/C++/Objective-C/Swift 源码文件到 Xcode 工程中。

    Add the C/C++/Objective-C/Swift source files to the Xcode project.

在 Android 中,您需要创建一个 CMakeLists.txt 文件用来定义如何编译源文件,同时告诉 Gradle 如何去定位它们。在项目根目录下,运行如下代码:

On Android, you need to create a CMakeLists.txt file to define how the sources should be compiled and point Gradle to it. From the root of your project directory, use the following instructions

cat > android/CMakeLists.txt << EOF
cmake_minimum_required(VERSION 3.4.1)  # for example

add_library( native_add

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             ../ios/Classes/native_add.cpp )
EOF

最后,添加一个 externalNativeBuild 到您的 android/build.gradle 文件中。示例如下:

Finally, add an externalNativeBuild section to android/build.gradle. For example:

android {
  // ...
  externalNativeBuild {
    // Encapsulates your CMake build configurations.
    cmake {
      // Provides a relative path to your CMake build script.
      path "CMakeLists.txt"
    }
  }
  // ...
}

步骤 3:在 FFI 库中读取代码

Step 3: Load the code using the FFI library

在示例中,您需要添加如下的代码到 lib/native_add.dart。但是,Dart 在何处进行代码绑定并不重要。

In this example, you can add the following code to lib/native_add.dart. However the location of the Dart binding code is not important.

首先,您需要创建一个 DynamicLibrary 来处理本地代码。这一步在 iOS 和 Android 之间有所不同:

First, you must create a DynamicLibrary handle to the native code. This step varies between iOS and Android:

import 'dart:ffi'; // For FFI
import 'dart:io'; // For Platform.isX

final DynamicLibrary nativeAddLib = Platform.isAndroid
    ? DynamicLibrary.open('libnative_add.so')
    : DynamicLibrary.process();

请注意,在 Android 上,本地库的名称是定义在 CMakeLists.txt 中的(见上文),但在 iOS 上,它将使用插件的名称。

Note that on Android the native library is named in CMakeLists.txt (see above), but on iOS it takes the plugin’s name.

您可以通过使用库的句柄来解析 native_add 符号:

With a handle to the enclosing library, you can resolve the native_add symbol:

final int Function(int x, int y) nativeAdd = nativeAddLib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('native_add')
    .asFunction();

现在,您可以调用它了。在自动生成的 example 项目(example/lib/main.dart)中演示它。

Finally, you can call it. To demonstrate this within the auto-generated “example” app (example/lib/main.dart):

// Inside of _MyAppState.build:
        body: Center(
          child: Text('1 + 2 == ${nativeAdd(1, 2)}'),
        ),

其他的用例

Other use cases

iOS 和 macOS

iOS and macOS

动态链接库在应用程序启动时由动态链接器自动加载。它们的组成符号可以用 DynamicLibrary.process。您还可以使用 DynamicLibrary.open 来限制符号解析的范围,但目前仍然不确定苹果的审查程序将如何处理两者的使用。

Dynamically linked libraries are automatically loaded by the dynamic linker when the app starts. Their constituent symbols can be resolved using DynamicLibrary.process. You can also get a handle to the library with DynamicLibrary.open to restrict the scope of symbol resolution, but it’s unclear how Apple’s review process handles this.

您可以使用 DynamicLibrary.executableDynamicLibrary.process 解析静态链接到应用程序二进制文件的符号。

Symbols statically linked into the application binary can be resolved using DynamicLibrary.executable or DynamicLibrary.process.

平台库

Platform library

要链接到平台库,请按照如下说明:

To link against a platform library, use the following instructions:

  1. 在 Xcode 中,打开 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace.

  2. 选择目标设备。

    Select the target platform.

  3. Linked Frameworks and Libraries 中点击 +

    Click + in the Linked Frameworks and Libraries section.

  4. 选择要链接的系统库。

    Select the system library to link against.

第一方库

First-party library

第一方本地库可以作为源文件或(已签名的).framework 文件被包含在内。它也可能包括静态链接的档案,但需要测试。

A first-party native library can be included either as source or as a (signed) .framework file. It’s probably possible to include statically linked archives as well, but it requires testing.

源码

Source code

要直接链接到源代码,请按照如下说明:

To link directly to source code, use the following instructions:

  1. 在 Xcode 中,打开 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace.

  2. 添加 C/C++/Objective-C/Swift 源码到 Xcode 工程中。

    Add the C/C++/Objective-C/Swift source files to the Xcode project.

  3. 将以下前缀添加到导出的符号声明中,以确保它们对 Dart 可见:

    Add the following prefix to the exported symbol declarations to ensure they are visible to Dart:

    C/C++/Objective-C

    extern "C" /* <= C++ only */ __attribute__((visibility("default"))) __attribute__((used))
    

    Swift

    @_cdecl("myFunctionName")
    

已编译的动态库

Compiled (dynamic) library

要链接到已编译过的动态库,请按照如下说明:

To link to a compiled dynamic library, use the following instructions:

  1. 如果存在已进行签名的 Framework 文件,请打开 Runner.xcworkspace

    If a properly signed Framework file is present, open Runner.xcworkspace.

  2. 添加 framework 文件到 Embedded Binaries 区域中。

    Add the framework file to the Embedded Binaries section.

  3. 同时将其添加到 Xcode 中目标的 Linked Frameworks & Libraries 部分。

    Also add it to the Linked Frameworks & Libraries section of the target in Xcode.

已编译的(动态)库 (macOS)

Compiled (dynamic) library (macOS)

要添加一个闭源的库到 Flutter macOS 桌面 应用,请按照如下说明:

To create add a closed source library to a Flutter macOS Desktop app, use the following instructions.

  1. 按照 Flutter 桌面的使用说明来创建 Flutter 桌面应用程序。

    Follow the instructions for Flutter desktop to create a Flutter desktop app.

  2. 在 Xcode 中打开 yourapp/macos/Runner.xcworkspace

    Open the yourapp/macos/Runner.xcworkspace in Xcode.

    1. 拖动您已经预编译的 libyourlibrary.dylib 到您的 Runner/Frameworks

      Drag your precompiled library (libyourlibrary.dylib) into Runner/Frameworks.

    2. 点击 Runner 然后进入 Build Phases 标签.

      Click Runner and go to the Build Phases tab.

      1. 拖动 libyourlibrary.dylibCopy Bundle Resources 列表。

        Drag libyourlibrary.dylib into the Copy Bundle Resources list.

      2. Embed Libararies 下,检查 Code Sign on Copy

        Under Embed Libraries, check Code Sign on Copy.

      3. Link Binary With Libraries 下,设置状态为 Optional。(咱们使用动态链接,不需要静态链接)

        Under Link Binary With Libraries, set status to Optional. (We use dynamic linking, no need to statically link.)

    3. 点击 Runner 然后进入 General 标签页。

      Click Runner and go to the General tab.

      1. 拖动 libyourlibrary.dylibFrameworks, Libararies and Embedded Content 列表中。

        Drag libyourlibrary.dylib into the Frameworks, Libararies and Embedded Content list.

      2. 选择 Embed & Sign

        Select Embed & Sign.

    4. 点击 Runner 然后进入 Build Settings 标签页。

      Click Runner and go to the Build Settings tab.

      1. Search Paths 部分,配置 Library Search Paths 确保 libyourlibrary.dylib 的路径包括在内。

        In the Search Paths section configure the Library Search Paths to include the path where libyourlibrary.dylib is located.

  3. 编辑 lib/main.dart 文件。

    Edit lib/main.dart.

    1. 使用 DynamicLibrary.open('libyourlibrary.dylib') 来动态链接符号表。

      Use DynamicLibrary.open('libyourlibrary.dylib') to dynamically link to the symbols.

    2. 在 widget 的某个地方调用您的本地代码。

      Call your native function somewhere in a widget.

  4. 运行 flutter run 然后检查您的本地方法的调用结果。

    Run flutter run and check that your native function gets called.

  5. 运行 flutter build macos 去构建一个自包含的 release 版本的应用。

    Run flutter build macos to build a selfcontained release version of your app.

开源的三方库

Open-source third-party library

要创建一个包含 C/C++/Objective-C Dart 代码的 Flutter 插件,请按照如下说明:

To create a Flutter plugin that includes both C/C++/Objective-C and Dart code, use the following instructions:

  1. 在您的插件项目打开 ios/<myproject>.podspec.

    In your plugin project, open ios/<myproject>.podspec.

  2. 添加本地代码到 source_files 字段。

    Add the native code to the source_files field.

本地代码会被静态链接到任何使用这个插件的应用二进制中。

The native code is then statically linked into the application binary of any app that uses this plugin.

闭源三方库

Closed-source third-party library

要创建包含 Dart 源代码,但 C/C++ 部分是以二进制形式分发的库的 Flutter 插件,请按照如下说明:

To create a Flutter plugin that includes Dart source code, but distribute the C/C++ library in binary form, use the following instructions:

  1. 在您的插件目录打开 ios/<myproject>.podspec

    In your plugin project, open ios/<myproject>.podspec.

  2. 添加 vendored_frameworks 字段。参考 CocoaPods 示例

    Add a vendored_frameworks field. See the CocoaPods example.

不要将此插件(或任何包含二进制代码的插件)上载到 pub.dev。相反,应该从可信的第三方下载此插件。如 CocoaPods 示例所示。

Do not upload this plugin (or any plugin containing binary code) to pub.dev. Instead, this plugin should be downloaded from a trusted third-party, as shown in the CocoaPods example.

Android

平台库

Platform library

如果要链接一个平台库,请按照如下说明:

To link against a platform library, use the following instructions:

  1. 在 Android 文档的 Android NDK Native APIs 列表中找到所需的库。它列出了稳定的本地 API。

    Find the desired library in the Android NDK Native APIs list in the Android docs. This lists stable native APIs.

  2. 使用 DynamicLibrary.open 加载库。

    Load the library using DynamicLibrary.open.

    示例:加载 OpenGL ES (v3):

    For example, to load OpenGL ES (v3):

    DynamicLibrary.open('libGLES_v3.so');
    

如果文档中有说明,您还需要根据说明更新 Android 应用程序或插件的清单文件。

You might need to update the Android manifest file of the app or plugin if indicated by the documentation.

第一方库

First-party library

对于应用程序或插件,以源代码或二进制形式包含本机代码的过程是相同的。

The process for including native code in source code or binary form is the same for an app or plugin.

开源三方库

Open-source third-party

遵循安卓文档中的 添加 C 和 C++ 代码到项目 来添加本地代码和对本地代码工具链的支持(CMake 或 ndk-build)。

Follow the Add C and C++ code to your project instructions in the Android docs to add native code and support for the native code toolchain (either CMake or ndk-build).

闭源三方库

Closed-source third-party library

要创建包含 Dart 源代码,但以二进制形式分发 C/C++ 库的 Flutter 插件,请按照如下说明:

To create a Flutter plugin that includes Dart source code, but distribute the C/C++ library in binary form, use the following instructions:

  1. 打开您项目的 android/build.gradle 文件。

    Open the android/build.gradle file for your project.

  2. 添加 aar 工件添加为依赖。 不要在您的 Flutter package 中导入工件。对应的,它需要在一个仓库中下载,比如 JCenter。

    Add the AAR artifact as a dependency. Don’t include the artifact in your Flutter package. Instead, it should be downloaded from a repository, such as JCenter.

Web

目前不支持 Web 插件。

This feature is not yet supported for web plugins.

FAQ

Android APK 尺寸(共享对象压缩)

Android APK size (shared object compression)

Android 指南 通常建议分发未压缩的本地共享对象,因为这种做法实际上可以节省设备空间。共享对象可以直接从 APK 加载,而不是将它们解压到设备上的临时位置然后再加载。 APK 是在传输过程中额外打包的 - 这就是为什么您应该查看下载的文件尺寸。

Android guidelines in general recommend distributing native shared objects uncompressed because that actually saves on device space. Shared objects can be directly loaded from the APK instead of unpacking them on device into a temporary location and then loading. APKs are additionally packed in transit - that is why you should be looking at download size.

Flutter APK 文件默认情况下不遵循这些指导原则来压缩 libflutter.solibapp.so,这会导致 APK 体积更小,但在设备上体积更大。

Flutter APKs by default don’t follow these guidelines and compress libflutter.so and libapp.so - this leads to smaller APK size but larger on device size.

来自第三方的共享库可以使用其 AndroidManifest.xml 中的 android:extractNativeLibs="true" 更改此默认设置,来停止压缩 libflutter.solibapp.so 和任何用户添加的共享库。要重新启用压缩,请按照如下方式重写您的 your_app_name/android/app/src/main/AndroidManifest.xml

Shared objects from third parties can change this default setting with android:extractNativeLibs="true" in their AndroidManifest.xml and stop the compression of libflutter.so, libapp.so, and any user-added shared objects. To re-enable compression, override the setting in your_app_name/android/app/src/main/AndroidManifest.xml in the following way.

@@ -1,5 +1,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.your_app_name">
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.example.your_app_name" >
     <!-- io.flutter.app.FlutterApplication is an android.app.Application that
          calls FlutterMain.startInitialization(this); in its onCreate method.
          In most cases you can leave this as-is, but you if you want to provide
          additional functionality it is fine to subclass or reimplement
          FlutterApplication and put your custom class here. -->
@@ -8,7 +9,9 @@
     <application
         android:name="io.flutter.app.FlutterApplication"
         android:label="your_app_name"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/ic_launcher"
+        android:extractNativeLibs="true"
+        tools:replace="android:extractNativeLibs">

删除的 iOS 符号

iOS symbols stripped

当创建一个 release 档案(IPA)时,符号会被 Xcode 删除。

When creating a release archive (IPA) the symbols are stripped by Xcode.

  1. 在 Xcode 中, 点击 Target Runner > Build Settings > Strip Style.

    In Xcode, go to Target Runner > Build Settings > Strip Style.

  2. All Symbols 修改为 Non-Global Symbols

    Change from All Symbols to Non-Global Symbols.

在 Flutter 应用中使用集成平台视图托管您的原生 Android 和 iOS 视图

目录

集成平台视图(后称为平台视图)允许将原生视图嵌入到 Flutter 应用中,所以您可以通过 Dart 将变换、裁剪和不透明度等效果应用到原生视图。

Platform views allow you to embed native views in a Flutter app, so you can apply transforms, clips, and opacity to the native view from Dart.

例如,这使您可以通过使用平台视图直接在 Flutter 应用内部使用 Android 和 iOS SDK 中的 Google Maps。

This allows you, for example, to use the native Google Maps from the Android and iOS SDKs directly inside your Flutter app, by using Platform Views.

本篇文档讨论了如何在您的 Flutter 应用中托管您的原生视图。

This page discusses how to host your own native views within a Flutter app.

Android

Flutter 支持两种集成模式:虚拟显示模式 (Virtual displays) 和混合集成模式 (Hybrid composition) 。

Flutter supports two modes: Virtual displays and Hybrid composition.

我们应根据具体情况来决定使用哪种模式。让我们来看看:

Which one to use depends on the use case. Let’s take a look:

在 Android 上创建平台视图需要如下的步骤:

To create a platform view on Android, follow these steps:

在 Dart 中进行的处理

On the Dart side

在 Dart 端,创建一个 Widget 然后添加如下的实现,具体如下:

On the Dart side, create a Widget and add the following build implementation:

混合集成模式

Hybrid composition

在 Dart 文件中,例如 native_view_example.dart,请执行下列操作:

In your Dart file, for example native_view_example.dart, do the following:

  1. 添加下面的导入:

    Add the following imports:

    import 'package:flutter/foundation.dart';
    import 'package:flutter/gestures.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter/services.dart';
    
  2. 实现 build 方法:

    Implement a build() method:

    Widget build(BuildContext context) {
      // This is used in the platform side to register the view.
      final String viewType = '<platform-view-type>';
      // Pass parameters to the platform side.
      final Map<String, dynamic> creationParams = <String, dynamic>{};
    
      return PlatformViewLink(
        viewType: viewType,
        surfaceFactory:
            (BuildContext context, PlatformViewController controller) {
          return AndroidViewSurface(
            controller: controller as AndroidViewController,
            gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
            hitTestBehavior: PlatformViewHitTestBehavior.opaque,
          );
        },
        onCreatePlatformView: (PlatformViewCreationParams params) {
          return PlatformViewsService.initSurfaceAndroidView(
            id: params.id,
            viewType: viewType,
            layoutDirection: TextDirection.ltr,
            creationParams: creationParams,
            creationParamsCodec: StandardMessageCodec(),
            onFocus: () {
              params.onFocusChanged(true);
            } ,
          )
            ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
            ..create();
        },
      );
    }
    

更多信息可以查看下面的 API 文档:

For more information, see the API docs for:

Virtual Display

在 Dart 文件中,例如 native_view_example.dart,请执行下列操作:

In your Dart file, for example native_view_example.dart, do the following:

  1. 添加下面的导入:

    Add the following imports:

    import 'package:flutter/widget.dart';
    
  2. 实现 build 方法:

    Implement a build() method:

    Widget build(BuildContext context) {
      // This is used in the platform side to register the view.
      final String viewType = 'hybrid-view-type';
      // Pass parameters to the platform side.
      final Map<String, dynamic> creationParams = <String, dynamic>{};
    
      return AndroidView(
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: const StandardMessageCodec(),
      );
    }
    

更多信息,查阅 API 文档:

For more information, see the API docs for:

在平台(Android)端

On the platform side

在平台端,使用 Java 或 Kotlin 中的标准包 io.flutter.plugin.platform

On the platform side, use the standard io.flutter.plugin.platform package in either Java or Kotlin:

在您的原生代码中,实现如下方法:

In your native code, implement the following:

继承 io.flutter.plugin.platform.PlatformView 以提供对 android.view.View 的引用,如 NativeView.kt 所示:

Extend io.flutter.plugin.platform.PlatformView to provide a reference to the android.view.View, For example NativeView.kt:

package dev.flutter.example

import android.content.Context
import android.graphics.Color
import android.view.View
import android.widget.TextView
import io.flutter.plugin.platform.PlatformView

internal class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val textView: TextView

    override fun getView(): View {
        return textView
    }

    override fun dispose() {}

    init {
        textView = TextView(context)
        textView.textSize = 72f
        textView.setBackgroundColor(Color.rgb(255, 255, 255))
        textView.text = "Rendered on a native Android view (id: $id)"
    }
}

创建一个用来创建 NativeView 的实例的工厂类,参考 NativeViewFactory.kt

Create a factory class that creates an instance of the NativeView created earlier, for example NativeViewFactory.kt:

package dev.flutter.example

import android.content.Context
import android.view.View
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory

class NativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
        val creationParams = args as Map<String?, Any?>?
        return NativeView(context, viewId, creationParams)
    }
}

最后,注册这个平台视图。这一步可以在应用中,也可以在插件中。

Finally, register the platform view. This can be done in an app or a plugin.

要在应用中进行注册,修改应用的主 Activity (例如:MainActivity.kt):

For app registration, modify the app’s main activity (for example, MainActivity.kt):

package dev.flutter.example

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine
                .platformViewsController
                .registry
                .registerViewFactory("<platform-view-type>", NativeViewFactory())
    }
}

要在插件中进行注册,修改您插件的主类(例如:PlatformViewPlugin.kt):

For plugin registration, modify the plugin’s main class (for example, PlatformViewPlugin.kt):

package dev.flutter.plugin.example

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding

class PlatformViewPlugin : FlutterPlugin {
    override fun onAttachedToEngine(binding: FlutterPluginBinding) {
        binding
                .platformViewRegistry
                .registerViewFactory("<platform-view-type>", NativeViewFactory())
    }

    override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
}

在您的原生代码中,实现如下方法:

In your native code, implement the following:

继承 io.flutter.plugin.platform.PlatformView 以提供对 android.view.View 的引用,如 NativeView.java 所示:

Extend io.flutter.plugin.platform.PlatformView to provide a reference to the android.view.View, For example, NativeView.java:

package dev.flutter.example;

import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.platform.PlatformView;
import java.util.Map;

class NativeView implements PlatformView {
   @NonNull private final TextView textView;

    NativeView(@NonNull Context context, int id, @Nullable Map<String, Object> creationParams) {
        textView = new TextView(context);
        textView.setTextSize(72);
        textView.setBackgroundColor(Color.rgb(255, 255, 255));
        textView.setText("Rendered on a native Android view (id: " + id + ")");
    }

    @NonNull
    @Override
    public View getView() {
        return textView;
    }

    @Override
    public void dispose() {}
}

创建一个用来创建 NativeView 的实例的工厂类,参考 NativeViewFactory.java

Create a factory class that creates an instance of the NativeView created earlier, for example, NativeViewFactory.java:

package dev.flutter.example;

import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;

class NativeViewFactory extends PlatformViewFactory {
  @NonNull private final BinaryMessenger messenger;
  @NonNull private final View containerView;

  NativeViewFactory(@NonNull BinaryMessenger messenger, @NonNull View containerView) {
    super(StandardMessageCodec.INSTANCE);
    this.messenger = messenger;
    this.containerView = containerView;
  }

  @NonNull
  @Override
  public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
    final Map<String, Object> creationParams = (Map<String, Object>) args;
    return new NativeView(context, id, creationParams);
  }
}

最后,注册这个平台视图。这一步可以在应用中,也可以在插件中。

Finally, register the platform view. This can be done in an app or a plugin.

要在应用中进行注册,修改应用的主 Activity (例如:MainActivity.java):

For app registration, modify the app’s main activity (for example, MainActivity.java):

package dev.flutter.example;

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;

public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        flutterEngine
            .getPlatformViewsController()
            .getRegistry()
            .registerViewFactory("<platform-view-type>", new NativeViewFactory());
    }
}

要在插件中进行注册,修改插件的主类(例如:PlatformViewPlugin.java):

For plugin registration, modify the plugin’s main file (for example, PlatformViewPlugin.java):

package dev.flutter.plugin.example;

import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;

public class PlatformViewPlugin implements FlutterPlugin {
  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
    binding
        .getPlatformViewRegistry()
        .registerViewFactory("<platform-view-type>", new NativeViewFactory());
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
}

更多信息,请查看 API 文档:

For more information, see the API docs for:

最后,修改您的 build.gradle 文件来满足 Android SDK 最低版本的要求:

Finally, modify your build.gradle file to require one of the minimal Android SDK versions:

android {
    defaultConfig {
        minSdkVersion 19 // if using Hybrid composition.
        minSdkVersion 20 // if using Virtual display.
    }
}

iOS

iOS 只支持混合集成模式,这意味着原生的 UIView 会被加入视图层级中。

iOS only uses Hybrid composition, which means that the native UIView is appended to view hierarchy.

在 Flutter 1.22 前,平台视图是一个开发者预览的版本。在 1.22 或更高版本中则不再是这样了,所以我们不再需要在 Info.plist 中设置 io.flutter.embedded_views_preview 标识。

Prior to Flutter 1.22, platform views were in developers preview. In 1.22 or above, this is no longer the case, so there’s no need to set the io.flutter.embedded_views_preview flag in Info.plist.

要在 iOS 中创建平台视图,需要如下步骤:

To create a platform view on iOS, follow these steps:

在 Dart 端

On the Dart side

在 Dart 端,创建一个 Widget 并添加如下的实现,具体如下:

On the Dart side, create a Widget and add the following build implementation, as shown in the following steps.

在 Dart 文件中,例如 native_view_example.dart,请执行下列操作:

In your Dart file, for example do the following in native_view_example.dart:

  1. 添加如下导入:

    Add the following imports:

    import 'package:flutter/widgets.dart';
    
  2. 实现 build 方法:

    Implement a build() method:

    Widget build(BuildContext context) {
      // This is used in the platform side to register the view.
      final String viewType = '<platform-view-type>';
      // Pass parameters to the platform side.
      final Map<String, dynamic> creationParams = <String, dynamic>{};
    
      return UiKitView(
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: const StandardMessageCodec(),
      );
    }
    

更多信息,请查看 API 文档: UIKitView.

For more information, see the API docs for: UIKitView.

在平台(iOS)端

On the platform side

在平台端,您可以使用 Swift 或是 Objective-C:

On the platform side, you use the either Swift or Objective-C:

实现工厂和平台视图。 FLNativeViewFactory 创建一个关联了 UIView 的平台视图。举个例子,FLNativeView.swift

Implement the factory and the platform view. The FLNativeViewFactory creates the platform view, and the platform view provides a reference to the UIView. For example, FLNativeView.swift:

import Flutter
import UIKit

class FLNativeViewFactory: NSObject, FlutterPlatformViewFactory {
    private var messenger: FlutterBinaryMessenger

    init(messenger: FlutterBinaryMessenger) {
        self.messenger = messenger
        super.init()
    }

    func create(
        withFrame frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?
    ) -> FlutterPlatformView {
        return FLNativeView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger)
    }
}

class FLNativeView: NSObject, FlutterPlatformView {
    private var _view: UIView

    init(
        frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?,
        binaryMessenger messenger: FlutterBinaryMessenger?
    ) {
        _view = UIView()
        super.init()
        // iOS views can be created here
        createNativeView(view: _view)
    }

    func view() -> UIView {
        return _view
    }

    func createNativeView(view _view: UIView){
        _view.backgroundColor = UIColor.blue
        let nativeLabel = UILabel()
        nativeLabel.text = "Native text from iOS"
        nativeLabel.textColor = UIColor.white
        nativeLabel.textAlignment = .center
        nativeLabel.frame = CGRect(x: 0, y: 0, width: 180, height: 48.0)
        _view.addSubview(nativeLabel)
    }
}

最后,注册这个平台视图。这一步可以在应用中,也可以在插件中。

Finally, register the platform view. This can be done in an app or a plugin.

要在应用中进行注册,修改应用中的 AppDelegate.swift

For app registration, modify the App’s AppDelegate.swift:

import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)

        weak var registrar = self.registrar(forPlugin: "plugin-name")

        let factory = FLNativeViewFactory(messenger: registrar!.messenger())
        self.registrar(forPlugin: "<plugin-name>")!.register(
            factory,
            withId: "<platform-view-type>")
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

要在插件中进行注册,修改插件的主类(例如 FLPlugin.swift):

For plugin registration, modify the plugin’s main file (for example, FLPlugin.swift):

import Flutter
import UIKit

class FLPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let factory = FLNativeViewFactory(messenger: registrar.messenger)
        registrar.register(factory, withId: "<platform-view-type>")
    }
}

在工厂类和平台视图的文件头部添加以下内容。用 FLNativeView.h 举例:

Add the headers for the factory and the platform view. For example, FLNativeView.h:

#import <Flutter/Flutter.h>

@interface FLNativeViewFactory : NSObject <FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end

@interface FLNativeView : NSObject <FlutterPlatformView>

- (instancetype)initWithFrame:(CGRect)frame
               viewIdentifier:(int64_t)viewId
                    arguments:(id _Nullable)args
              binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;

- (UIView*)view;
@end

实现工厂类和平台视图。 FLNativeViewFactory 创建一个关联了 UIView 的平台视图。用 FLNativeView.m 举例:

Implement the factory and the platform view. The FLNativeViewFactory creates the platform view, and the platform view provides a reference to the UIView. For example, FLNativeView.m:

#import "FLNativeView.h"

@implementation FLNativeViewFactory {
  NSObject<FlutterBinaryMessenger>* _messenger;
}

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
  self = [super init];
  if (self) {
    _messenger = messenger;
  }
  return self;
}

- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
                                   viewIdentifier:(int64_t)viewId
                                        arguments:(id _Nullable)args {
  return [[FLNativeView alloc] initWithFrame:frame
                              viewIdentifier:viewId
                                   arguments:args
                             binaryMessenger:_messenger];
}

@end

@implementation FLNativeView {
   UIView *_view;
}

- (instancetype)initWithFrame:(CGRect)frame
               viewIdentifier:(int64_t)viewId
                    arguments:(id _Nullable)args
              binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
  if (self = [super init]) {
    _view = [[UIView alloc] init];
  }
  return self;
}

- (UIView*)view {
  return _view;
}

@end

最后,注册这个平台视图。这一步可以在应用中,也可以在插件中。

Finally, register the platform view. This can be done in an app or a plugin.

要在应用中进行注册,修改应用中的 AppDelegate.m

For app registration, modify the App’s AppDelegate.m:

#import "AppDelegate.h"
#import "FLNativeView.h"
#import "GeneratedPluginRegistrant.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];

   NSObject<FlutterPluginRegistrar>* registrar =
      [self registrarForPlugin:@"plugin-name"];

  FLNativeViewFactory* factory =
      [[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger];

  [[self registrarForPlugin:@"<plugin-name>"] registerViewFactory:factory
                                                          withId:@"<platform-view-type>"];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

要在插件中进行注册,修改插件主文件(例如 FLPlugin.m):

For plugin registration, modify the main plugin file (for example, FLPlugin.m):

#import <Flutter/Flutter.h>
#import "FLNativeView.h"

@interface FLPlugin : NSObject<FlutterPlugin>
@end

@implementation FLPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FLNativeViewFactory* factory =
      [[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger];
  [registrar registerViewFactory:factory withId:@"<platform-view-type>"];
}

@end

更多信息,请查看 API 文档:

For more information, see the API docs for:

整合

Putting it together

在 Dart 中实现 build() 方法时,您可以使用 defaultTargetPlatform 来检测当前的平台,并且决定如何使用这个 widget:

When implementing the build() method in Dart, you can use defaultTargetPlatform to detect the platform, and decide what widget to use:

Widget build(BuildContext context) {
  // This is used in the platform side to register the view.
  final String viewType = '<platform-view-type>';
  // Pass parameters to the platform side.
  final Map<String, dynamic> creationParams = <String, dynamic>{};

  switch (defaultTargetPlatform) {
    case TargetPlatform.android:
      // return widget on Android.
    case TargetPlatform.iOS:
      // return widget on iOS.
    default:
      throw UnsupportedError("Unsupported platform view");
  }
}

性能

Performance

在 Flutter 中使用平台视图时,性能会有所折衷。

Platform views in Flutter come with performance trade-offs.

例如,在典型的 Flutter 应用中,Flutter UI 是在专用栅格线程上组成的。由于平台的主线程很少被阻塞,因此 Flutter 应用程序可以快速运行。

For example, in a typical Flutter app, the Flutter UI is composed on a dedicated raster thread. This allows Flutter apps to be fast, as the main platform thread is rarely blocked.

使用混合集成模式渲染平台视图时, Flutter UI 由平台线程完成,与其他线程一起竞争,例如:处理系统或插件消息等任务。

While a platform view is rendered with Hybrid composition, the Flutter UI is composed from the platform thread, which competes with other tasks like handling OS or plugin messages.

在 Android 10 之前,混合集成模式将每个 Flutter 帧从显存中复制到主内存中,然后再将其复制回 GPU 纹理中。在 Android 10 或更高版本中,显存会被复制两次。由于每帧都会进行一次复制,因此可能会影响整个 Flutter UI 的性能。

Prior to Android 10, Hybrid composition copies each Flutter frame out of the graphic memory into main memory, and then copies it back to a GPU texture. In Android 10 or above, the graphics memory is copied twice. As this copy happens per frame, the performance of the entire Flutter UI may be impacted.

另一方面,虚拟显示模式使平台视图的每个像素流经附加的中间图形缓冲区,这会浪费显存和绘图性能。

Virtual display, on the other hand, makes each pixel of the native view flow through additional intermediate graphic buffers, which cost graphic memory and drawing performance.

对于复杂的情况,可以使用一些技巧来缓解这些问题。

For complex cases, there are some techniques that can be used to mitigate these issues.

例如,当 Dart 中发生动画时,您可以使用占位符纹理。换句话说,如果在渲染平台视图时动画很慢,请考虑对原生视图进行截图,并将其渲染为纹理。

For example, you could use a placeholder texture while an animation is happening in Dart. In other words, if an animation is slow while a platform view is rendered, then consider taking a screenshot of the native view and rendering it as a texture.

更多消息,查看:

For more information, see:

Web 常见问题

目录

Web 版本的 Flutter 是否已经准备好投入生产环境中了呢?

Is the web version of Flutter ready for production?

Flutter 网页端的支持正式在稳定版渠道发布,提供了以应用为中心的框架,框架以现代 Web 平台的功能为基础,如果希望了解更多,请查看文章 Flutter Web 支持现已进入稳定版

Flutter’s web support is now available on the stable channel, offering an app-centric framework that builds on the power of the modern web platform. Find out more details about Flutter’s production quality support for the web.

在 Web 平台使用 Flutter 的场景有哪些?

What scenarios are ideal for Flutter on the web?

Flutter 目前并非适用于所有的网页内容,不过我们主要关注三个应用场景:

Not every web page makes sense in Flutter, but we think Flutter is particularly suited for app-centric experiences:

现在阶段,Flutter 不适合具有丰富文本和瀑布流的页面。例如,博客文章等基于流媒体的丰富文本内容,其受益于网络构建的以文档为中心的模式,而不是像 Flutter 这样的 UI 框架可以提供的以应用为中心的服务。然而,你可以使用 Flutter 将交互式体验嵌入到这些网站中。

At this time, Flutter is not suitable for static websites with text-rich flow-based content. For example, blog articles benefit from the document-centric model that the web is built around, rather than the app-centric services that a UI framework like Flutter can deliver. However, you can use Flutter to embed interactive experiences into these websites.

有关如何在 Web 上使用 Flutter 的更多信息,参考文档: Flutter 的 Web 支持

For more information on how you can use Flutter on the web, see Web support for Flutter.

我应该如何提交 web 支持相关的 issue

How do I file an issue about web support?

你可以在 Flutter 主仓库中 发起一个 issue。请确保标题中包含了 “web” 关键字。

You can file an issue on the main Flutter repo. Make sure that “web” is included in the title.

如何创建同时在 Web 上运行的应用?

How do I create an app that also runs on the web?

请参见 使用 Flutter 构建 Web 应用

See building a web app with Flutter.

我该如何在浏览器中刷新正在运行的应用?

Does hot reload work with a web app?

不能,但是可以使用热重启 (hot restart)。热重启是可以您的应用快速响应改动的方法,无需等待重新编译的载入。它与移动端的热重载功能类似。唯一的区别是热重载可以保持应用的状态。

No, but you can use hot restart. Hot restart is a fast way of seeing your changes without having to relaunch your web app and wait for it to compile and load. This works similarly to the hot reload feature for Flutter mobile development. The only difference is that hot reload remembers your state and hot restart doesn’t.

我该如何在浏览器中重启正在运行的应用?

How do I restart the app running in the browser?

使用浏览器的刷新按钮不会起作用,但你可以在执行 flutter run -d chrome 的控制台中输入「R」进行刷新。

Using the browser’s refresh button doesn’t work, but you can enter “R” in the console where “flutter run -d chrome” is running.

现在有哪些浏览器支持 Flutter 了?

Which web browsers are supported by Flutter?

现在 Flutter web 应用可以运行在以下浏览器中:

Flutter web apps can run on the following browsers:

在开发阶段,Chrome(在 macOS、Windows 以及 Linux)以及 Edge(在 Windows 上)将作为默认浏览器用于调试。

During development, Chrome (on macOS, Windows, and Linux) and Edge (on Windows) are supported as the default browsers for debugging your app.

我可以在任意 IDE 中,构建、运行并发布 web 应用吗?

Can I build, run, and deploy web apps in any of the IDEs?

你可以在 Android Studio/IntelliJ 和 VS Code 里选择使用 Chrome 或者 Edge 浏览器。

You can select Chrome or Edge as the target device in Android Studio/IntelliJ and VS Code.

设备下拉列表里现在应该在所有平台里都包含了 Chrome (web)。

The device pulldown should now include the Chrome (web) option for all channels.

我该如何构建响应式 web 应用?

How do I build a responsive app for the web?

请参阅 创建响应式应用

See Creating responsive apps.

我能使用 Flutter 插件么?

Can I use Flutter plugins?

是的,目前有很多插件已经支持了 web。在 pub.dev 上使用 web 过滤以找到更新的插件清单。你也可以为已有的或者 你自己编写的 plugin 添加 web 支持。

Yes, several plugins have web support. Find an updated list of plugins on pub.dev using the web filter. You can also add web support to existing plugins or write your own plugins for the web.

我能在 Web 应用中使用 dart:io 这个 package 吗?

Can I use dart:io with a web app?

不行。文件系统在浏览器中是无法访问的。对于网络功能来说,请使用 http package。请注意,安全方面的工作有所不同,因为浏览器(而不是应用程序)控制 HTTP 请求上的标头。

No. The file system is not accessible from the browser. For network functionality, use the http package. Note that security works somewhat differently because the browser (and not the app) controls the headers on an HTTP request.

我应该如何处理一个 Web 平台特定的导入?

How do I handle web-specific imports?

部分插件需要在特定平台导入库或者文件,尤其是当使用浏览器无法访问的文件系统时。若要在你的应用里使用这些插件,请参阅 dart.cn选择性的导入

Some plugins require platform-specific imports, particularly if they use the file system, which is not accessible from the browser. To use these plugins in your app, see the documentation for conditional imports on dart.dev.

我该如何把一个 Flutter web 应用嵌入到一个网页中?

How do I embed a Flutter web app in a web page?

你可以通过下面这个例子,以 iframe 来内嵌,把 URL 替换成托管 Flutter Web 的页面 URL:

You can embed a Flutter web app, as you would embed other content, in an iframe tag of an HTML file. In the following example, replace “URL” with the location of your hosted HTML page:

<iframe src="URL"></iframe>

如果你遇到问题,请 提交一个 issue 给我们。

If you encounter problems, please file an issue.

我该如何调试一个 web 应用?

How do I debug a web app?

使用 Flutter DevTools 来尝试如下工作:

Use Flutter DevTools for the following tasks:

使用 Chrome DevTools 来尝试如下工作:

Use Chrome DevTools for the following tasks:

我该如何测试 Web 应用?

How do I test a web app?

使用常规的 widget tests,了解更多关于如何在浏览器里使用集成测试,请查看 集成测试 文档页面。

Use widget tests or integration tests. To learn more about running integration tests in a browser, see the Integration testing page.

How do I internationalize a web app?

Flutter 移动应用的国际化 无差别。

This isn’t any different than internationalizing a Flutter mobile app.

我该如何部署 Web 应用?

How do I deploy a web app?

请参阅 打包并发布到 Web 平台

See Preparing a web app for release.

Platform.is API 现在可用吗?

Does Platform.is work on the web?

目前还不行。

Not currently.

如何跟其他使用者交流?

How can I compare notes with others who are playing with this feature?

请在 Discord 平台的 #web 这个讨论板跟大家讨论, Flutter 团队的工程师会经常阅读和互动。

Check out the #web discussion channel on Discord. Flutter engineers routinely read and respond on Discord.

撰写双端平台代码(插件编写实现)

目录

本指南介绍了如何编写自定义的平台相关代码,某些平台相关功能可通过已有的软件包获得,具体细节可查看: 在 Flutter 里使用 Packages

This guide describes how to write custom platform-specific code. Some platform-specific functionality is available through existing packages; see using packages.

Flutter 使用了灵活的系统,它允许你调用相关平台的 API,无论是 Android 中的 Java 或 Kotlin 代码,还是 iOS 中的 Objective-C 或 Swift 代码。

Flutter uses a flexible system that allows you to call platform-specific APIs whether available in Kotlin or Java code on Android, or in Swift or Objective-C code on iOS.

Flutter uses a flexible system that allows you to call platform-specific APIs in a language that works directly with those APIs:

Flutter 使用了灵活系统,无论是在 Android 上的 Kotlin 还是 Java,亦或是 iOS 上的 Swift 或 Objective-C,它都允许你调用平台特定 API。

Flutter 内置的平台特定 API 支持不依赖于任何生成代码,而是灵活的依赖于传递消息格式。或者,你也可以使用 Pigeon 这个 package,通过生成代码来 发送结构化类型安全消息

Flutter’s builtin platform-specific API support does not rely on code generation, but rather on a flexible message passing style. Alternatively, the package Pigeon can be used for sending structured typesafe messages via code generation:

架构概述:平台通道

Architectural overview: platform channels

消息使用平台通道在客户端(UI)和宿主(平台)之间传递,如下图所示:

Messages are passed between the client (UI) and host (platform) using platform channels as illustrated in this diagram:

Platform channels architecture

消息和响应以异步的形式进行传递,以确保用户界面能够保持响应。

Messages and responses are passed asynchronously, to ensure the user interface remains responsive.

客户端做方法调用的时候 MethodChannel 会负责响应,从平台一侧来讲,Android 系统上使用 MethodChannelAndroid、 iOS 系统使用 MethodChanneliOS 来接收和返回来自 MethodChannel 的方法调用。在开发平台插件的时候,可以减少样板代码。

On the client side, MethodChannel enables sending messages that correspond to method calls. On the platform side, MethodChannel on Android (MethodChannelAndroid) and FlutterMethodChannel on iOS (MethodChanneliOS) enable receiving method calls and sending back a result. These classes allow you to develop a platform plugin with very little ‘boilerplate’ code.

平台通道数据类型及编解码器

Platform channel data types support and codecs

标准平台通道使用标准消息编解码器,它支持简单的类似 JSON 值的高效二进制序列化,例如布尔值、数字、字符串、字节缓冲区及这些类型的列表和映射(详情请参阅 StandardMessageCodec)。当你发送和接收值时,它会自动对这些值进行序列化和反序列化。

The standard platform channels use a standard message codec that supports efficient binary serialization of simple JSON-like values, such as booleans, numbers, Strings, byte buffers, and Lists and Maps of these (see StandardMessageCodec for details). The serialization and deserialization of these values to and from messages happens automatically when you send and receive values.

下表展示了如何在平台端接收 Dart 值,反之亦然:

The following table shows how Dart values are received on the platform side and vice versa:

Dart Java
null null
bool java.lang.Boolean
int java.lang.Integer
int, if 32 bits not enough java.lang.Long
double java.lang.Double
String java.lang.String
Uint8List byte[]
Int32List int[]
Int64List long[]
Float32List float[]
Float64List double[]
List java.util.ArrayList
Map java.util.HashMap
Dart Kotlin
null null
bool Boolean
int Int
int, if 32 bits not enough Long
double Double
String String
Uint8List ByteArray
Int32List IntArray
Int64List LongArray
Float32List FloatArray
Float64List DoubleArray
List List
Map HashMap
Dart Objective-C
null nil (NSNull when nested)
bool NSNumber numberWithBool:
int NSNumber numberWithInt:
int, if 32 bits not enough NSNumber numberWithLong:
double NSNumber numberWithDouble:
String NSString
Uint8List FlutterStandardTypedData typedDataWithBytes:
Int32List FlutterStandardTypedData typedDataWithInt32:
Int64List FlutterStandardTypedData typedDataWithInt64:
Float32List FlutterStandardTypedData typedDataWithFloat32:
Float64List FlutterStandardTypedData typedDataWithFloat64:
List NSArray
Map NSDictionary
Dart Swift
null nil
bool NSNumber(value: Bool)
int NSNumber(value: Int32)
int, if 32 bits not enough NSNumber(value: Int)
double NSNumber(value: Double)
String String
Uint8List FlutterStandardTypedData(bytes: Data)
Int32List FlutterStandardTypedData(int32: Data)
Int64List FlutterStandardTypedData(int64: Data)
Float32List FlutterStandardTypedData(float32: Data)
Float64List FlutterStandardTypedData(float64: Data)
List Array
Map Dictionary
Dart C++
null EncodableValue()
bool EncodableValue(bool)
int EncodableValue(int32_t)
int, if 32 bits not enough EncodableValue(int64_t)
double EncodableValue(double)
String EncodableValue(std::string)
Uint8List EncodableValue(std::vector)
Int32List EncodableValue(std::vector)
Int64List EncodableValue(std::vector)
Float32List EncodableValue(std::vector)
Float64List EncodableValue(std::vector)
List EncodableValue(std::vector)
Map EncodableValue(std::map<EncodableValue, EncodableValue>)
Dart C (GObject)
null FlValue()
bool FlValue(bool)
int FlValue(int62_t)
double FlValue(double)
String FlValue(gchar*)
Uint8List FlValue(uint8_t*)
Int32List FlValue(int32_t*)
Int64List FlValue(int64_t*)
Float32List FlValue(float*)
Float64List FlValue(double*)
List FlValue(FlValue)
Map FlValue(FlValue, FlValue)

示例: 通过平台通道调用平台的 iOS 和 Android 代码

Example: Calling platform-specific iOS and Android code using platform channels

以下代码演示了如何调用平台相关 API 来检索并显示当前的电池电量。它通过平台消息 getBatteryLevel() 来调用 Android 的 BatteryManager API 及 iOS 的 device.batteryLevel API。

The following code demonstrates how to call a platform-specific API to retrieve and display the current battery level. It uses the Android BatteryManager API, and the iOS device.batteryLevel API, via a single platform message, getBatteryLevel().

该示例在主应用程序中添加平台相关代码。如果想要将该代码重用于多个应用程序,那么项目的创建步骤将略有差异(查看 Flutter Packages 的开发和提交),但平台通道代码仍以相同方式编写。

The example adds the platform-specific code inside the main app itself. If you want to reuse the platform-specific code for multiple apps, the project creation step is slightly different (see developing packages), but the platform channel code is still written in the same way.

注意:可在 /examples/platform_channel/ 中获得使用 Java 实现的 Android 及使用 Objective-C 实现的 iOS 的该示例完整可运行的代码。对于用 Swift 实现的 iOS 代码,请参阅 /examples/platform_channel_swift/

Note: The full, runnable source-code for this example is available in /examples/platform_channel/ for Android with Java and iOS with Objective-C. For iOS with Swift, see /examples/platform_channel_swift/.

第一步:创建一个新的应用项目

Step 1: Create a new app project

首先创建一个新的应用:

Start by creating a new app:

默认情况下,我们的模板使用 Kotlin 编写 Android 或使用 Swift 编写 iOS 代码。要使用 Java 或 Objective-C,请使用 -i 和/或 -a 标志:

By default our template supports writing Android code using Kotlin, or iOS code using Swift. To use Java or Objective-C, use the -i and/or -a flags:

第二步:创建 Flutter 平台客户端

Step 2: Create the Flutter platform client

应用程序的 State 类保持当前应用的状态。扩展它以保持当前的电池状态。

The app’s State class holds the current app state. Extend that to hold the current battery state.

首先,构建通道。在返回电池电量的单一平台方法中使用 MethodChannel

First, construct the channel. Use a MethodChannel with a single platform method that returns the battery level.

通道的客户端和宿主端通过传递给通道构造函数的通道名称进行连接。一个应用中所使用的所有通道名称必须是唯一的;使用唯一的 域前缀 为通道名称添加前缀,比如:samples.flutter.dev/battery

The client and host sides of a channel are connected through a channel name passed in the channel constructor. All channel names used in a single app must be unique; prefix the channel name with a unique ‘domain prefix’, for example: samples.flutter.dev/battery.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // Get battery level.

接下来,在方法通道上调用方法(指定通过 String 标识符 getBatteryLevel 调用的具体方法)。调用可能会失败—比如,如果平台不支持此平台 API(比如在模拟器中运行),所以将 invokeMethod 调用包裹在 try-catch 语句中。

Next, invoke a method on the method channel, specifying the concrete method to call using the String identifier getBatteryLevel. The call might fail—for example if the platform does not support the platform API (such as when running in a simulator), so wrap the invokeMethod call in a try-catch statement.

setState 中使用返回结果来更新 _batteryLevel 内的用户界面状态。

Use the returned result to update the user interface state in _batteryLevel inside setState.

// Get battery level.
String _batteryLevel = 'Unknown battery level.';

Future<void> _getBatteryLevel() async {
  String batteryLevel;
  try {
    final int result = await platform.invokeMethod('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
  } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
  }

  setState(() {
    _batteryLevel = batteryLevel;
  });
}

最后,将模板中的 build 方法替换为包含以字符串形式显示电池状态、并包含一个用于刷新该值的按钮的小型用户界面。

Finally, replace the build method from the template to contain a small user interface that displays the battery state in a string, and a button for refreshing the value.

@override
Widget build(BuildContext context) {
  return Material(
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(
            child: const Text('Get Battery Level'),
            onPressed: _getBatteryLevel,
          ),
          Text(_batteryLevel),
        ],
      ),
    ),
  );
}

步骤 3: 添加 Android 平台的实现

Step 3: Add an Android platform-specific implementation

首先在 Android Studio 中打开 Flutter 应用的 Android 宿主部分:

Start by opening the Android host portion of your Flutter app in Android Studio:

  1. 启动 Android Studio

    Start Android Studio

  2. 选择菜单项 File > Open…

    Select the menu item File > Open…

  3. 导航到包含 Flutter 应用的目录,然后选择其中的 android 文件夹。点击 OK

    Navigate to the directory holding your Flutter app, and select the android folder inside it. Click OK.

  4. 在项目视图中打开 kotlin 文件夹下的 MainActivity.kt 文件(注意:如果使用 Android Studio 2.3 进行编辑,请注意 kotlin 目录的显示名称为 java)。

    Open the file MainActivity.kt located in the kotlin folder in the Project view. (Note: If editing with Android Studio 2.3, note that the kotlin folder is shown as if named java.)

configureFlutterEngine() 方法中创建一个 MethodChannel 并调用 setMethodCallHandler()。确保使用的通道名称与 Flutter 客户端使用的一致。

Inside the configureFlutterEngine() method, create a MethodChannel and call setMethodCallHandler(). Make sure to use the same channel name as was used on the Flutter client side.

import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // Note: this method is invoked on the main thread.
      // TODO
    }
  }
}

添加使用 Android battery API 来检索电池电量的 Android Kotlin 代码。该代码与你在原生 Android 应用中编写的代码完全相同。

Add the Android Kotlin code that uses the Android battery APIs to retrieve the battery level. This code is exactly the same as you would write in a native Android app.

首先在文件头部添加所需的依赖:

First, add the needed imports at the top of the file:

import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

然后在 MainActivity 类中的 configureFlutterEngine() 方法下方添加以下新方法:

Next, add the following method in the MainActivity class, below the configureFlutterEngine() method:

  private fun getBatteryLevel(): Int {
    val batteryLevel: Int
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    } else {
      val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    }

    return batteryLevel
  }

最后,完成前面添加的 onMethodCall() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数 call 中对其进行验证。该平台方法的实现是调用上一步编写的 Android 代码,并使用 result 参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

Finally, complete the setMethodCallHandler() method added earlier. You need to handle a single platform method, getBatteryLevel(), so test for that in the call argument. The implementation of this platform method calls the Android code written in the previous step, and returns a response for both the success and error cases using the result argument. If an unknown method is called, report that instead.

删除以下代码:

Remove the following code:

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // Note: this method is invoked on the main thread.
      // TODO
    }

并替换成以下内容:

And replace with the following:

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      // Note: this method is invoked on the main thread.
      call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()

        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      } else {
        result.notImplemented()
      }
    }

首先在 Android Studio 中打开 Flutter 应用的 Android 宿主部分:

Start by opening the Android host portion of your Flutter app in Android Studio:

  1. 启动 Android Studio

    Start Android Studio

  2. 选择菜单项 File > Open…

    Select the menu item File > Open…

  3. 导航到包含 Flutter 应用的目录,然后选择其中的 android 文件夹。点击 OK

    Navigate to the directory holding your Flutter app, and select the android folder inside it. Click OK.

  4. 在项目视图中打开 java 文件夹下的 MainActivity.java 文件。

    Open the MainActivity.java file located in the java folder in the Project view.

接下来,在 configureFlutterEngine() 方法中创建一个 MethodChannel 并设置一个 MethodCallHandler。确保使用的通道名称与 Flutter 客户端使用的一致。

Next, create a MethodChannel and set a MethodCallHandler inside the configureFlutterEngine() method. Make sure to use the same channel name as was used on the Flutter client side.

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "samples.flutter.dev/battery";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
  super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // Note: this method is invoked on the main thread.
            // TODO
          }
        );
  }
}

添加使用 Android battery API 来检索电池电量的 Android Java 代码。该代码与你在原生 Android 应用中编写的代码完全相同。

Add the Android Java code that uses the Android battery APIs to retrieve the battery level. This code is exactly the same as you would write in a native Android app.

首先在文件头部添加所需的依赖:

First, add the needed imports at the top of the file:

import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;

然后在 Activity 类中的 onCreate() 方法下方添加以下新方法:

Then add the following as a new method in the activity class, below the configureFlutterEngine() method:

  private int getBatteryLevel() {
    int batteryLevel = -1;
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    } else {
      Intent intent = new ContextWrapper(getApplicationContext()).
          registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
      batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
          intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    }

    return batteryLevel;
  }

最后,完成前面添加的 onMethodCall() 方法,你需要处理单个平台方法 getBatteryLevel(),所以在参数 call 中对其进行验证。该平台方法的实现是调用上一步编写的 Android 代码,并使用 result 参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

Finally, complete the setMethodCallHandler() method added earlier. You need to handle a single platform method, getBatteryLevel(), so test for that in the call argument. The implementation of this platform method calls the Android code written in the previous step, and returns a response for both the success and error cases using the result argument. If an unknown method is called, report that instead.

移除以下代码:

Remove the following code:

          (call, result) -> {
            // Note: this method is invoked on the main thread.
            // TODO
          }

并替换成以下内容:

And replace with the following:

          (call, result) -> {
            // Note: this method is invoked on the main thread.
            if (call.method.equals("getBatteryLevel")) {
              int batteryLevel = getBatteryLevel();

              if (batteryLevel != -1) {
                result.success(batteryLevel);
              } else {
                result.error("UNAVAILABLE", "Battery level not available.", null);
              }
            } else {
              result.notImplemented();
            }
          }

现在你应该可以在 Android 中运行该应用。如果使用了 Android 模拟器,请在扩展控件面板中设置电池电量,可从工具栏中的 按钮访问。

You should now be able to run the app on Android. If using the Android Emulator, set the battery level in the Extended Controls panel accessible from the button in the toolbar.

步骤 4a:添加 iOS 平台的实现

Step 4a: Add an iOS platform-specific implementation

首先在 Xcode 中打开 Flutter 应用的 iOS 宿主部分:

Start by opening the iOS host portion of the Flutter app in Xcode:

  1. 启动 Xcode

    Start Xcode.

  2. 选择菜单项 File > Open…

    Select the menu item File > Open….

  3. 导航到包含 Flutter 应用的目录,然后选择其中的 ios 文件夹。点击 OK

    Navigate to the directory holding your Flutter app, and select the ios folder inside it. Click OK.

  4. 确保 Xcode 项目构建没有错误。

    Make sure the Xcode projects builds without errors.

  5. 打开项目导航 Runner > Runner 下的 AppDelegate.m 文件。

    Open the file AppDelegate.m, located under Runner > Runner in the Project navigator.

application didFinishLaunchingWithOptions: 方法中创建一个 FlutterMethodChannel 并添加一个处理程序。确保使用的通道名称与 Flutter 客户端使用的一致。

Create a FlutterMethodChannel and add a handler inside the application didFinishLaunchingWithOptions: method. Make sure to use the same channel name as was used on the Flutter client side.

#import <Flutter/Flutter.h>
#import "GeneratedPluginRegistrant.h"

@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;

  FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                          methodChannelWithName:@"samples.flutter.dev/battery"
                                          binaryMessenger:controller.binaryMessenger];

  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // Note: this method is invoked on the UI thread.
    // TODO
  }];

  [GeneratedPluginRegistrant registerWithRegistry:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

接下来添加使用 iOS battery API 来检索电池电量的 iOS Objective-C 代码。该代码与你在原生 iOS 应用中编写的代码完全相同。

Next, add the iOS ObjectiveC code that uses the iOS battery APIs to retrieve the battery level. This code is exactly the same as you would write in a native iOS app.

AppDelegate 类中的 @end 之前添加以下方法:

Add the following method in the AppDelegate class, just before @end:

- (int)getBatteryLevel {
  UIDevice* device = UIDevice.currentDevice;
  device.batteryMonitoringEnabled = YES;
  if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
  } else {
    return (int)(device.batteryLevel * 100);
  }
}

最后,完成前面添加的 setMethodCallHandler() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数call 中对其进行验证。该平台方法的实现是调用上一步编写的 iOS 代码,并使用 result 参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

Finally, complete the setMethodCallHandler() method added earlier. You need to handle a single platform method, getBatteryLevel(), so test for that in the call argument. The implementation of this platform method calls the iOS code written in the previous step, and returns a response for both the success and error cases using the result argument. If an unknown method is called, report that instead.

__weak typeof(self) weakSelf = self;
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  // Note: this method is invoked on the UI thread.
  if ([@"getBatteryLevel" isEqualToString:call.method]) {
    int batteryLevel = [weakSelf getBatteryLevel];

    if (batteryLevel == -1) {
      result([FlutterError errorWithCode:@"UNAVAILABLE"
                                 message:@"Battery info unavailable"
                                 details:nil]);
    } else {
      result(@(batteryLevel));
    }
  } else {
    result(FlutterMethodNotImplemented);
  }
}];

现在你应该可以在 iOS 中运行该应用。如果使用了 iOS 模拟器,注意它并不支持 battery API,并且应用会显示 ‘battery info unavailable’。

You should now be able to run the app on iOS. If using the iOS Simulator, note that it does not support battery APIs, and the app displays ‘battery info unavailable’.

步骤 4b:使用 Swift 添加 iOS 平台的实现

Step 4b: Add an iOS platform-specific implementation using Swift

注意:以下步骤与 4a 类似,唯一的区别是使用了 Swift 而非 Objective-C。

Note: The following steps are similar to step 4a, only using Swift rather than Objective-C.

此步骤假设你在第一步中使用 -i swift 选项创建了项目。

This step assumes that you created your project in step 1. using the -i swift option.

首先在 Xcode 中打开 Flutter 应用的 iOS 宿主部分:

Start by opening the iOS host portion of your Flutter app in Xcode:

  1. 启动 Xcode

    Start Xcode.

  2. 选择菜单项 File > Open…

    Select the menu item File > Open….

  3. 导航到包含 Flutter 应用的目录,然后选择其中的 ios 文件夹。点击 OK

    Navigate to the directory holding your Flutter app, and select the ios folder inside it. Click OK.

在使用 Objective-C 的标准模板设置中添加对 Swift 的支持:

Add support for Swift in the standard template setup that uses Objective-C:

  1. 在项目导航中展开 Expand Runner > Runner

    Expand Runner > Runner in the Project navigator.

  2. 打开项目导航 Runner > Runner 下的 AppDelegate.swift 文件。

    Open the file AppDelegate.swift located under Runner > Runner in the Project navigator.

重写 application:didFinishLaunchingWithOptions: 方法并创建一个绑定了通道名称 samples.flutter.dev/batteryFlutterMethodChannel

Override the application:didFinishLaunchingWithOptions: function and create a FlutterMethodChannel tied to the channel name samples.flutter.dev/battery:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      // Handle battery messages.
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

接下来添加使用 iOS battery API 来检索电池电量的 iOS Swift 代码。该代码与你在原生 iOS 应用中编写的代码完全相同。

Next, add the iOS Swift code that uses the iOS battery APIs to retrieve the battery level. This code is exactly the same as you would write in a native iOS app.

AppDelegate.swift 末尾添加以下新方法:

Add the following as a new method at the bottom of AppDelegate.swift:

private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery info unavailable",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}

最后,完成前面添加的 setMethodCallHandler() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数 call 中对其进行验证。该平台方法的实现是调用上一步编写的 iOS 代码。如果调用了未知方法,则报告该方法。

Finally, complete the setMethodCallHandler() method added earlier. You need to handle a single platform method, getBatteryLevel(), so test for that in the call argument. The implementation of this platform method calls the iOS code written in the previous step. If an unknown method is called, report that instead.

batteryChannel.setMethodCallHandler({
  [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
  // Note: this method is invoked on the UI thread.
  guard call.method == "getBatteryLevel" else {
    result(FlutterMethodNotImplemented)
    return
  }
  self?.receiveBatteryLevel(result: result)
})

现在你应该可以在 iOS 中运行该应用。如果使用了 iOS 模拟器,注意它并不支持 battery API,并且应用会显示 ‘battery info unavailable’。

You should now be able to run the app on iOS. If using the iOS Simulator, note that it does not support battery APIs, and the app displays ‘battery info unavailable’.

通过 Pigeon 获得类型安全的通道

Typesafe platform channels via Pigeon

在之前的样例中,我们使用 MethodChannel 在 host 和 client 之间进行通信,然而这并不是类型安全的。为了正确通信,调用/接收消息取决于 host 和 client 声明相同的参数和数据类型。 Pigeon 包可以用作 MethodChannel 的替代品,它将生成以结构化类型安全方式发送消息的代码。

The previous example uses MethodChannel to communicate between the host and client which isn’t typesafe. Calling and receiving messages depends on the host and client declaring the same arguments and datatypes in order for messages to work. The Pigeon package can be used as an alternative to MethodChannel to generate code that sends messages in a structured typesafe manner.

Pigeon 中,消息接口在 Dart 中进行定义,然后它将生成对应的 Android 以及 iOS 的代码。更复杂的例子以及更多信息尽在 Pigeon pub.dev page

With Pigeon the messaging protocol is defined in a subset of Dart which then generates messaging code for Android or iOS. A more complete example and more information can be found on the Pigeon pub.dev page;

使用 Pigeon 消除了在主机和客户端之间匹配字符串的需要消息的名称和数据类型。它支持:嵌套类,消息转换为 API,生成异步包装代码并发送消息。生成的代码具有相当的可读性并保证在不同版本的多个客户端之间没有冲突。支持 Objective-C,Java,Kotlin 和 Swift(通过 Objective-C 互操作)语言。

Using Pigeon eliminates the need to match strings between host and client for the names and datatypes of messages. It supports: nested classes, grouping messages into APIs, generation of asynchronous wrapper code and sending messages in either direction. The generated code is readable and guarantees there will be no conflicts between multiple clients of different versions. Supported languages are Objective-C, Java, Kotlin and Swift (via Objective-C interop).

Pigeon 样例

Pigeon example

Pigeon file:

import 'package:pigeon/pigeon.dart';

class SearchRequest {
  String query = '';
}

class SearchReply {
  String result = '';
}

@HostApi()
abstract class Api {
  Future search(SearchRequest request);
}

Dart usage:

import 'generated_pigeon.dart';

void onClick() async {
  SearchRequest request = SearchRequest()..query = 'test';
  Api api = SomeApi();
  SearchReply reply = await api.search(request);
  print('reply: ${reply.result}');
}

从 UI 代码中分离平台相关代码

Separate platform-specific code from UI code

如果你想要在多个 Flutter 应用中使用你的平台相关代码,则将代码分离为位于主应用目录之外的平台插件会很有用。相关细节查看 Flutter Packages 的开发和提交

If you expect to use your platform-specific code in multiple Flutter apps, it can be useful to separate the code into a platform plugin located in a directory outside your main application. See developing packages for details.

将平台相关代码作为 Package 进行提交

Publish platform-specific code as a package

与 Flutter 生态中的其他开发者共享你的平台相关代码,可查看 提交 package

To share your platform-specific code with other developers in the Flutter ecosystem, see publishing packages.

自定义通道和编解码器

Custom channels and codecs

除了上面提到的 MethodChannel,你还可以使用更基础的 BasicMessageChannel,它支持使用自定义的消息编解码器进行基本的异步消息传递。你还可以使用专门的 BinaryCodecStringCodecJSONMessageCodec 类,或创建自己的编解码器。

Besides the above mentioned MethodChannel, you can also use the more basic BasicMessageChannel, which supports basic, asynchronous message passing using a custom message codec. You can also use the specialized BinaryCodec, StringCodec, and JSONMessageCodec classes, or create your own codec.

您还可以在 cloud_firestore 插件中查看自定义编解码器的示例,该插件可以序列化和反序列化比默认类型更多的类型。

You might also check out an example of a custom codec in the cloud_firestore plugin, which is able to serialize and deserialize many more types than the default types.

通道和平台线程

Channels and Platform Threading

目标平台向 Flutter 发起 channel 调用的时候,需要在对应平台的主线程执行。同样的,在 Flutter 向目标平台发起 channel 调用的时候,需要在根 Isolate 中执行。对应平台侧的 handler 既可以在平台的主线程执行,也可以通过事件循环在后台执行。对应平台侧 handler 的返回值可以在任意线程异步执行。

When invoking channels on the platform side destined for Flutter, they need to be invoked on the platform’s main thread. When invoking channels in Flutter destined for the platform side, they need to be invoked on the root Isolate. The platform side’s handlers can execute on the platform’s main thread or they can execute on a background thread if a Task Queue is used. The result of the platform side handlers can be invoked asynchronously and on any thread.

在 Flutter 里使用 Packages

目录

Flutter 支持使用其他开发者向 Flutter 和 Dart 生态系统贡献的共享 package,这意味着你可以快速构建应用而不是一切从零开始。

Flutter supports using shared packages contributed by other developers to the Flutter and Dart ecosystems. This allows quickly building an app without having to develop everything from scratch.

现有的 package 支持许多使用场景,例如,网络请求 (http),自定义导航/路由处理 (fluro),集成设备 API(如 (url_launcherbattery,以及使用第三方平台的 SDK(如 Firebase 的 (FlutterFire)。

Existing packages enable many use cases—for example, making network requests (http), custom navigation/route handling (fluro), integration with device APIs (url_launcher and battery), and using third-party platform SDKs like Firebase (FlutterFire).

如果你正打算开发新的 package,请参阅 Flutter Packages 的开发和提交

To write a new package, see developing packages.

如果你想添加资源、图片或字体,无论是存储在文件中还是 package 中,请参阅 添加资源和图片 这篇文档。

To add assets, images or fonts, whether stored in files or packages, see Adding assets and images.

使用 package

Using packages

下面的内容将为你描述如何使用已经发布了的 packages。

The following section describes how to use existing published packages.

搜索 package

Searching for packages

Package 会被发布到 pub.dev 网站上。

Packages are published to pub.dev.

Pub 网站上的 Flutter 页面 展示了与 Flutter 兼容的 package(即声明的依赖通常与 Flutter 兼容),并且所有已发布的 package 都支持搜索。

The Flutter landing page on pub.dev displays top packages that are compatible with Flutter (those that declare dependencies generally compatible with Flutter), and supports searching among all published packages.

Pub.dev 上的 Flutter Favorites 页面列出了一系列编写应用时可以首先考虑使用的插件和 package,关于这个项目的更多信息,请查看 Flutter Favorites 项目 页面。

The Flutter Favorites page on pub.dev lists the plugins and packages that have been identified as packages you should first consider using when writing your app. For more information on what it means to be a Flutter Favorite, see the Flutter Favorites program.

在 pub.dev 网站上同时可以过滤出适合 AndroidiOSWeb 的插件,也可以通过复选框,过滤出组合结果(适配一个或者多个平台)。

You can also browse the packages on pub.dev by filtering on Android plugins, iOS plugins, web plugins, or any combination thereof.

将 package 依赖添加到应用

Adding a package dependency to an app

要将 package ‘css_colors’ 添加到应用:

To add the package, css_colors, to an app:

  1. 添加依赖

    Depend on it

    • 打开应用文件夹下的 pubspec.yaml 文件,然后在 pubspec.yaml 下添加 css_colors:

      Open the pubspec.yaml file located inside the app folder, and add css_colors: under dependencies.

  2. 安装

    Install it

    • 在命令行中运行:flutter pub get

      From the terminal: Run flutter pub get

    或者

    OR

    • 在 Android Studio/IntelliJ 中点击 pubspec.yaml 文件顶部操作功能区的 Packages get

      From Android Studio/IntelliJ: Click Packages get in the action ribbon at the top of pubspec.yaml.

    • 在 VS Code 中点击位于 pubspec.yaml 文件顶部操作功能区右侧的 Get Packages

      From VS Code: Click Get Packages located in right side of the action ribbon at the top of pubspec.yaml.

  3. 导入

    Import it

    • 在 Dart 代码中添加相关的 import 语句。

      Add a corresponding import statement in the Dart code.

  4. 如果有必要,停止并重启应用

    Stop and restart the app, if necessary

    • 如果 package 内有特定平台的代码(Android 的 Java/Kotlin, iOS 的 Swift/Objective-C),代码必须内置到你的应用内。热重载和热重启只对 package 的 Dart 代码执行此操作,所以你需要完全重启应用以避免使用 package 时出现 MissingPluginException 错误。

      If the package brings platform-specific code (Kotlin/Java for Android, Swift/Objective-C for iOS), that code must be built into your app. Hot reload and hot restart only update the Dart code, so a full restart of the app might be required to avoid errors like MissingPluginException when using the package.

对于这些步骤,Pub 上任何 package 页面的 Installing tab 选项卡都是一个很方便的参考。

The Installing tab, available on any package page on pub.dev, is a handy reference for these steps.

完整示例,参阅下面的 css_colors 示例

For a complete example, see the css_colors example below.

冲突解决

Conflict resolution

假设你想在应用中使用 some_packageother_package,并且它们依赖于不同版本的 url_launcher。于是我们便有了潜在的冲突。避免这种情况的最好方法是 package 的作者在指定依赖项时使用 版本范围 而非特定版本。

Suppose you want to use some_package and another_package in an app, and both of these depend on url_launcher, but in different versions. That causes a potential conflict. The best way to avoid this is for package authors to use version ranges rather than specific versions when specifying dependencies.

dependencies:
  url_launcher: ^5.4.0    # Good, any version >= 5.4.0 but < 6.0.0
  image_picker: '5.4.3'   # Not so good, only version 5.4.3 works.

如果 some_package 声明了以上依赖,并且 another_package 声明了一个兼容的 url_launcher 依赖项,如 '5.4.6'^5.5.0, pub 能够自动解决冲突问题。 Gradle modulesCocoaPods 也是用类似的方式解决平台依赖的。

If some_package declares the dependencies above and another_package declares a compatible url_launcher dependency like '5.4.6' or ^5.5.0, pub resolves the issue automatically. Platform-specific dependencies on Gradle modules and/or CocoaPods are solved in a similar way.

即使 some_packageanother_package 声明了不兼容的 url_launcher 版本,它们实际上仍可能以兼容的方式使用 url_launcher。在这种情况下,可在 pubspec.yaml 文件中添加一个依赖覆盖声明来强制使用特定版本,从而处理冲突。

Even if some_package and another_package declare incompatible versions for url_launcher, they might actually use url_launcher in compatible ways. In this situation, the conflict can be resolved by adding a dependency override declaration to the app’s pubspec.yaml file, forcing the use of a particular version.

为了强制使用版本为 5.4.0url_launcher,你可以对应用的 pubspec.yaml 文件做如下更改:

For example, to force the use of url_launcher version 5.4.0, make the following changes to the app’s pubspec.yaml file:

dependencies:
  some_package:
  another_package:
dependency_overrides:
  url_launcher: '5.4.0'

如果依赖冲突项不是 package 自身,而是如 guava 这样特定于 Android 的库,那么依赖的覆盖声明必须添加到 Gradle 的构建逻辑中。

如果依赖冲突项不是 package 自身,而是如 guava 这样特定于 Android 的库,那么依赖的覆盖声明必须添加到 Gradle 的构建逻辑中。

If the conflicting dependency is not itself a package, but an Android-specific library like guava, the dependency override declaration must be added to Gradle build logic instead.

为了强制使用版本为 28.0guava,你可以对 android/build.gradle 文件做如下更改:

To force the use of guava version 28.0, make the following changes to the app’s android/build.gradle file:

configurations.all {
    resolutionStrategy {
        force 'com.google.guava:guava:28.0-android'
    }
}

CocoaPods 目前尚不提供依赖项覆盖功能。

CocoaPods does not currently offer dependency override functionality.

开发新的 package

Developing new packages

如果某个 package 不适用于你的特定需求,你可以 开发新的自定义 package

If no package exists for your specific use case, you can write a custom package.

管理 package 的依赖和版本

Managing package dependencies and versions

为了使版本冲突的风险最小化,请在 pubspec.yaml 文件中指定一个版本范围。

To minimize the risk of version collisions, specify a version range in the pubspec.yaml file.

Package 版本

Package versions

所有 package 都有一个版本号,在它们的 pubspec.yaml 文件中指定。当前的 package 版本会在其名称旁边显示当前版本号。(例如,参阅 url_launcher package)以及所有先前版本的列表: url_launcher 版本列表

All packages have a version number, specified in the package’s pubspec.yaml file. The current version of a package is displayed next to its name (for example, see the url_launcher package), as well as a list of all prior versions (see url_launcher versions).

当使用简写形式 plugin1: 将 package 添加到 pubspec.yaml 时,表明 plugin1 package 的任何版本都可以被使用。为了确保在更新 package 的时候你的应用不会崩溃,我们建议使用以下格式之一来指定版本范围:

When a package is added to pubspec.yaml, the shorthand form plugin1: means that any version of the plugin1 package can be used. To ensure that the app doesn’t break when a package is updated, specify a version range using one of the following formats:

了解更详细的信息,参阅 Pub 版本管理指南

For additional details, see the package versioning guide.

更新 package 依赖

Updating package dependencies

当你添加一个 package 后首次运行 flutter pub get(IntelliJ 或 Android Studio 中的 Packages Get), Flutter 将会保存在 pubspec.lock lockfile 中找到的具体 package 版本。这将确保当你或者团队中其他开发者运行 flutter pub get 后能得到相同版本的 package。

When running flutter pub get (Packages get in IntelliJ or Android Studio) for the first time after adding a package, Flutter saves the concrete package version found in the pubspec.lock lockfile. This ensures that you get the same version again if you, or another developer on your team, run flutter pub get.

如果你想升级到 package 的最新版本,比如使用 package 的最新特性,请运行 flutter packages upgrade (IntelliJ 或 Android Studio 的 Upgrade dependencies 功能)。这将检索你在 pubspec.yaml 文件中指定的版本约束所允许的最高可用版本。请注意,flutter upgradeflutter update-packages 是两个不同的命令,但它们都会更新 Flutter。

To upgrade to a new version of the package, for example to use new features in that package, run flutter pub upgrade (Upgrade dependencies in IntelliJ or Android Studio) to retrieve the highest available version of the package that is allowed by the version constraint specified in pubspec.yaml. Note that this is a different command from flutter upgrade or flutter update-packages, which both update Flutter itself.

依赖未发布的 package

Dependencies on unpublished packages

即使未在 Pub site 上发布,也可以使用 package。对于不用于公开发布的私有插件,或者尚未准备好发布的 package,可以使用其他依赖选项。

Packages can be used even when not published on pub.dev. For private plugins, or for packages not ready for publishing, additional dependency options are available:

Path 依赖
Flutter 应用可以通过文件系统 path: 依赖而依赖于插件。路径可以是相对的,也可以是绝对的。例如,要依赖位于应用相邻目录中的插件 plugin1,可以使用以下语法:

Path dependency
A Flutter app can depend on a plugin via a file system path: dependency. The path can be either relative or absolute. Relative paths are evaluated relative to the directory containing pubspec.yaml. For example, to depend on a plugin plugin1 located in a directory next to the app, use the following syntax:

  dependencies:
    plugin1:
      path: ../plugin1/

Git 依赖
你也可以依赖存储在 Git 仓库中的 package,如果 package 位于仓库的根目录,可以使用以下语法:

Git dependency
You can also depend on a package stored in a Git repository. If the package is located at the root of the repo, use the following syntax:

  dependencies:
    plugin1:
      git:
        url: git://github.com/flutter/plugin1.git

**Git 依赖于文件夹中的 package **
默认情况下,pub 会默认假定 package 位于 Git 仓库的根目录。如果不是这种情况,你可以使用 path 参数指定位置,例如:

Git dependency on a package in a folder
Pub assumes the package is located in the root of the Git repository. If that is not the case, specify the location with the path argument. For example:

  dependencies:
    package1:
      git:
        url: git://github.com/flutter/packages.git
        path: packages/package1

最后,你可以使用 ref 参数将依赖固定到 git 特定的 commit、branch 或者 tag。更多详细信息,请参阅 Package dependencies

Finally, use the ref argument to pin the dependency to a specific git commit, branch, or tag. For more details, see Package dependencies.

例子

Examples

下面的示例将介绍使用 packages 的一些必要步骤。

The following examples walk through the necessary steps for using packages.

例子:使用 CSS Colors package

Example: Using the CSS Colors package

css_colors package 为 CSS 颜色定义颜色常量,允许你在 Flutter 框架中任何需要 Color 类型的地方使用它们。

The css_colors package defines color constants for CSS colors, so use the constants wherever the Flutter framework expects the Color type.

要使用这个 package:

To use this package:

  1. 创建一个名为 cssdemo 的新项目

    Create a new project called cssdemo.

  2. 打开 pubspec.yaml,并添加依赖 css-colors

    Open pubspec.yaml, and add the css-colors dependency:

    dependencies:
      flutter:
        sdk: flutter
    

    替换为:

    with:

    dependencies:
      flutter:
        sdk: flutter
      css_colors: ^1.0.0
    
  3. 在命令行中运行 flutter packages get,或者点击 Intellij 中的 Packages get

    Run flutter pub get in the terminal, or click Packages get in IntelliJ or Android Studio.

  4. 打开 lib/main.dart 并将其全部内容替换为:

    Open lib/main.dart and replace its full contents with:

    import 'package:css_colors/css_colors.dart';
     import 'package:flutter/material.dart';
    
     void main() {
       runApp(const MyApp());
     }
    
     class MyApp extends StatelessWidget {
       const MyApp({Key? key}) : super(key: key);
    
       @override
       Widget build(BuildContext context) {
         return const MaterialApp(
           home: DemoPage(),
         );
       }
     }
    
     class DemoPage extends StatelessWidget {
       const DemoPage({Key? key}) : super(key: key);
    
       @override
       Widget build(BuildContext context) {
         return Scaffold(body: Container(color: CSSColors.orange));
       }
     }
  5. 运行应用。当你点击 ‘Show Flutter homepage’ 时,你将看到手机默认浏览器打开并出现 Flutter 主页。

    Run the app. The app’s background should now be orange.

例子:使用 url_launcher package 来打开浏览器

Example: Using the url_launcher package to launch the browser

url_launcher 插件可以让你在移动平台上打开默认浏览器以显示给定的 URL。它演示了 package 如何也可能包含特定于平台的代码我们将这一类包含各平台不同代码的 package 称为 插件 package 或者 插件

The url_launcher plugin package enables opening the default browser on the mobile platform to display a given URL, and is supported on Android, iOS, web, and macos. This package is a special Dart package called a plugin package (or plugin), which includes platform-specific code.

要使用这个插件:

To use this plugin:

  1. 新建一个名为 lauchdemo 的新项目;

    Create a new project called launchdemo.

  2. 打开 pubspec.yaml,然后添加 url_launcher 的依赖:

    Open pubspec.yaml, and add the url_launcher dependency:

    dependencies:
      flutter:
        sdk: flutter
      url_launcher: ^5.4.0
    
  3. 在命令行中运行 flutter packages get,或者点击 Intellij 或 Android Studio 中的 Packages get

    Run flutter pub get in the terminal, or click Packages get in IntelliJ or Android Studio.

  4. 打开 lib/main.dart 并将其全部内容替换为:

    Open lib/main.dart and replace its full contents with the following:

    import 'package:flutter/material.dart';
     import 'package:url_launcher/url_launcher.dart';
    
     void main() {
       runApp(const MyApp());
     }
    
     class MyApp extends StatelessWidget {
       const MyApp({Key? key}) : super(key: key);
    
       @override
       Widget build(BuildContext context) {
         return const MaterialApp(
           home: DemoPage(),
         );
       }
     }
    
     class DemoPage extends StatelessWidget {
       const DemoPage({Key? key}) : super(key: key);
    
       launchURL() {
         launch('https://flutter.dev');
       }
    
       @override
       Widget build(BuildContext context) {
         return Scaffold(
           body: Center(
             child: ElevatedButton(
               onPressed: launchURL,
               child: const Text('Show Flutter homepage'),
             ),
           ),
         );
       }
     }
  5. 运行应用(如果你的应用在添加插件之前已经运行,请停止并重启应用)。当你点击 Show Flutter homepage 时,你将看到手机默认浏览器打开并出现 Flutter 主页。

    Run the app (or stop and restart it, if it was already running before adding the plugin). Click Show Flutter homepage. You should see the default browser open on the device, displaying the homepage for flutter.dev.

Flutter Packages 的开发和提交

目录

插件 API 现已支持 联合插件,从而分离在不同平台上的实现。你可以指定 哪些平台有插件 支持,例如 Web 和 macOS。

The plugin API has been updated and now supports federated plugins that enable separation of different platform implementations. You can also now indicate which platforms a plugin supports, for example web and macOS.

旧的插件 API 会在将来被废弃。如果在短期内你仍在使用旧版本的插件 API,你会看到警告。想了解更多关于升级 Android 插件的内容,请阅读 支持新的 Android 插件 API

Eventually, the old plugin APIs will be deprecated. In the short term, you will see a warning when the framework detects that you are using an old-style plugin. For information on how to upgrade your plugin, see Supporting the new Android plugins APIs.

Package 介绍

Package introduction

通过使用 package(的模式)可以创建易于共享的模块化代码。一个最基本的 package 由以下内容构成:

Packages enable the creation of modular code that can be shared easily. A minimal package consists of the following:

pubspec.yaml 文件
用于定义 package 名称、版本号、作者等其他信息的元数据文件。

pubspec.yaml
A metadata file that declares the package name, version, author, and so on.

lib 目录
包含共享代码的 lib 目录,其中至少包含一个 <package-name>.dart 文件。

lib
The lib directory contains the public code in the package, minimally a single <package-name>.dart file.

Package 类别

Package types

Package 包含以下几种类别:

Packages can contain more than one kind of content:

纯 Dart 库 (Dart packages)
用 Dart 编写的传统 package,比如 path。其中一些可能包含 Flutter 的特定功能,因此依赖于 Flutter 框架,其使用范围仅限于 Flutter,比如 fluro

Dart packages
General packages written in Dart, for example the path package. Some of these might contain Flutter specific functionality and thus have a dependency on the Flutter framework, restricting their use to Flutter only, for example the fluro package.

原生插件 (Plugin packages)
使用 Dart 编写的,按需使用 Java 或 Kotlin、Objective-C 或 Swift 分别在 Android 和/或 iOS 平台实现的 package。

Plugin packages
A specialized Dart package that contains an API written in Dart code combined with one or more platform-specific implementations.

插件 package 可以针对 Android(使用 Kotlin 或 Java)、 iOS(使用 Swift 或 Objective-C)、Web、macOS、Windows 或 Linux,又或者它们的各种组合方式,进行编写。

Plugin packages can be written for Android (using Kotlin or Java), iOS (using Swift or Objective-C), web, macOS, Windows, or Linux, or any combination thereof.

一个较为具体的实现例子是 url_launcher 插件 package。想了解如何使用 url_launcher package,以及它如何扩展 Web 的实现,请阅读 Medium 上由 Harry Terkelsen 撰写的文章 如何编写 Flutter Web 插件,第一部分

A concrete example is the url_launcher plugin package. To see how to use the url_launcher package, and how it was extended to implement support for web, see the Medium article by Harry Terkelsen, How to Write a Flutter Web Plugin, Part 1.

开发纯 Dart 的 packages

Developing Dart packages

下面会为你介绍如何写 Flutter package。

The following instructions explain how to write a Flutter package.

第一步:创建 package

Step 1: Create the package

想要创建纯 Dart 库的 package,请使用带有 --template=package 标志的 flutter create 命令:

To create a Flutter package, use the --template=package flag with flutter create:

$ flutter create --template=package hello

这将在 hello 目录下创建一个 package 项目,其中包含以下内容:

This creates a package project in the hello folder with the following content:

LICENSE 文件
大概率会是空的一个许可证文件。

LICENSE
A (mostly) empty license text file.

test/hello_test.dart 文件
Package 的 单元测试 文件。

test/hello_test.dart
The unit tests for the package.

hello.iml 文件
由 IntelliJ 生成的配置文件。

hello.iml
A configuration file used by the IntelliJ IDEs.

.gitignore 文件
告诉 Git 系统应该隐藏哪些文件或文件夹的一个隐藏文件。

.gitignore
A hidden file that tells Git which files or folders to ignore in a project.

.metadata 文件
IDE 用来记录某个 Flutter 项目属性的的隐藏文件。

.metadata
A hidden file used by IDEs to track the properties of the Flutter project.

pubspec.yaml 文件
pub 工具需要使用的,包含 package 依赖的 yaml 格式的文件。

pubspec.yaml
A yaml file containing metadata that specifies the package’s dependencies. Used by the pub tool.

README.md 文件
起步文档,用于描述 package。

README.md
A starter markdown file that briefly describes the package’s purpose.

lib/hello.dart 文件
package 的 Dart 实现代码。

lib/hello.dart
A starter app containing Dart code for the package.

.idea/modules.xml.idea/workspace.xml 文件
IntelliJ 的各自配置文件(包含在 .idea 隐藏文件夹下)。

.idea/modules.xml, .idea/workspace.xml
A hidden folder containing configuration files for the IntelliJ IDEs.

CHANGELOG.md 文件
又一个大概率为空的文档,用于记录 package 的版本变更。

CHANGELOG.md
A (mostly) empty markdown file for tracking version changes to the package.

第二步:实现 package

Step 2: Implement the package

对于纯 Dart 库的 package,只要在 lib/<package name>.dart 文件中添加功能实现,或在 lib 目录中的多个文件中添加功能实现。

For pure Dart packages, simply add the functionality inside the main lib/<package name>.dart file, or in several files in the lib directory.

如果要对 package 进行测试,在 test 目录下添加 单元测试

To test the package, add unit tests in a test directory.

关于如何组织 package 内容的更多详细信息,请参考 Dart library package 文档。

For additional details on how to organize the package contents, see the Dart library package documentation.

开发原生插件类型的 packages

Developing plugin packages

如果想要开发一个调用特定平台 API 的 package,你需要开发一个原生插件 packgae。

If you want to develop a package that calls into platform-specific APIs, you need to develop a plugin package.

它的 API 通过 平台通道 连接到平台特定的实现。

The API is connected to the platform-specific implementation(s) using a platform channel.

联合插件

Federated plugins

Federated plugins (联合插件) 是一种将对不同平台的支持分为单独的软件包。所以,联合插件能够使用针对 iOS、Android、Web 甚至是针对汽车 (例如在 IoT 设备上)分别使用对应的 package。除了这些好处之外,它还能够让领域专家在他们最了解的平台上扩展现有平台插件。

Federated plugins are a way of splitting support for different platforms into separate packages. So, a federated plugin can use one package for iOS, another for Android, another for web, and yet another for a car (as an example of an IoT device). Among other benefits, this approach allows a domain expert to extend an existing plugin to work for the platform they know best.

联合插件需要以下 package:

A federated plugin requires the following packages:

面向应用的 package
该 package 是用户使用插件的的直接依赖。它指定了 Flutter 应用使用的 API。

app-facing package
The package that plugin users depend on to use the plugin. This package specifies the API used by the Flutter app.

平台 package
一个或多个包含特定平台代码的 package。面向应用的 package 会调用这些平台 package—— 除非它们带有一些终端用户需要的特殊平台功能,否则它们不会包含在应用中。

platform package(s)
One or more packages that contain the platform-specific implementation code. The app-facing package calls into these packages—they aren’t included into an app, unless they contain platform-specific functionality accessible to the end user.

平台接口 package
将面向应用的 package 与平台 package 进行整合的 package。该 package 会声明平台 package 需要实现的接口,供面向应用的 package 使用。使用单一的平台接口 package 可以确保所有平台 package 都按照各自的方法实现了统一要求的功能。

platform interface package
The package that glues the app-facing packing to the platform package(s). This package declares an interface that any platform package must implement to support the app-facing package. Having a single package that defines this interface ensures that all platform packages implement the same functionality in a uniform way.

整合的联合插件

Endorsed federated plugin

理想情况下,当你在为一个联合插件添加某个平台的实现时,你会与 package 的作者合作,将你的实现纳入 package。

Ideally, when adding a platform implementation to a federated plugin, you will coordinate with the package author to include your implementation. In this way, the original author endorses your implementation.

假设你开发了 foobar_windows 插件,用于对应 foobar 插件的实现。在整合的联合插件里,foobar 的原作者会将你的 Windows 实现作为依赖添加在 pubspec 文件中,供面向应用的 package 调用。而后在开发者使用 foobar 插件时,Windows 及已包含的其他平台的实现就自动可用了。

For example, say you write a foobar_windows implementation for the (imaginary) foobar plugin. In an endorsed plugin, the original foobar author adds your Windows implementation as a dependency in the pubspec for the app-facing package. Then, when a developer includes the foobar plugin in their Flutter app, the Windows implementation, as well as the other endorsed implementations, are automatically available to the app.

未整合的联合插件

Non-endorsed federated plugin

如果你的实现出于某些原因无法被原作者整合,那么你的插件属于 未整合 的联合插件。开发者仍然可以使用你的实现,但是必须手动在 pubspec 文件里添加引用。意味着开发者需要同时引用 foobar foobar_windows 依赖,才能使用对应平台的完整功能。

If you can’t, for whatever reason, get your implementation added by the original plugin author, then your plugin is not endorsed. A developer can still use your implementation, but must manually add the plugin to the app’s pubspec file. So, the developer must include both the foobar dependency and the foobar_windows dependency in order to achieve full functionality.

有关联合插件的更多信息、它为什么非常强大,以及如何实现联合插件,你可以阅读 Harry Terkelsen 在 Medium 撰写的 如何撰写 Flutter Web 插件,第 2 部分

For more information on federated plugins, why they are useful, and how they are implemented, see the Medium article by Harry Terkelsen, How To Write a Flutter Web Plugin, Part 2.

指定一个插件支持的平台

Specifying a plugin’s supported platforms

插件可以通过向 pubspec.yaml 中的 platforms map 添加 keys 来指定其支持的平台。例如,以下是 hello 插件的 flutter: map,它仅支持 Android 和 iOS:

Plugins can specify the platforms they support by adding keys to the platforms map in the pubspec.yaml file. For example, the following pubspec file shows the flutter: map for the hello plugin, which supports only iOS and Android:

flutter:
  plugin:
    platforms:
      android:
        package: com.example.hello
        pluginClass: HelloPlugin
      ios:
        pluginClass: HelloPlugin

environment:
  sdk: ">=2.1.0 <3.0.0"
  # Flutter versions prior to 1.12 did not support the
  # flutter.plugin.platforms map.
  flutter: ">=1.12.0"

当为更多平台添加插件实现时,应相应地更新 platforms map,例如这是支持 Android、iOS、macOS 和 web 的 hello 插件的 map:

When adding plugin implementations for more platforms, the platforms map should be updated accordingly. For example, here’s the map in the pubspec file for the hello plugin, when updated to add support for macOS and web:

flutter:
  plugin:
    platforms:
      android:
        package: com.example.hello
        pluginClass: HelloPlugin
      ios:
        pluginClass: HelloPlugin
      macos:
        pluginClass: HelloPlugin
      web:
        pluginClass: HelloPlugin
        fileName: hello_web.dart

environment:
  sdk: ">=2.1.0 <3.0.0"
  # Flutter versions prior to 1.12 did not support the
  # flutter.plugin.platforms map.
  flutter: ">=1.12.0"

联合平台 package

Federated platform packages

平台 package 有着同样的格式,但会包含 implements 入口,用于指明 package 实现的平台。例如,实现了 hello package 的 Windows 平台的 hello_windows 插件,会在 flutter: 映射下包含以下内容:

A platform package uses the same format, but includes an implements entry indicating which app-facing package it is an implementation for. For example, a hello_windows plugin containing the Windows implementation for hello would have the following flutter: map:

flutter:
  plugin:
    implements: hello
    platforms:
      windows:
        pluginClass: HelloPlugin

认可的实现

Endorsed implementations

提供给 App 项目使用的 package 可以通过在 platform: 映射下声明 default_package,认可一个平台实现插件。如果 hello 插件认可了 hello_windows,它看起来会是这样:

An app facing package can endorse a platform package by adding a dependency on it, and including it as a default_package in the platforms: map. If the hello plugin above endorsed hello_windows, it would look like this:

flutter:
  plugin:
    platforms:
      android:
        package: com.example.hello
        pluginClass: HelloPlugin
      ios:
        pluginClass: HelloPlugin
      windows:
        default_package: hello_windows

dependencies:
  hello_windows: ^1.0.0

注意如上所示,面向 App 项目的 package 可能已经包含了某些平台的实现,同时也有认可的其他平台的实现。

Note that as shown here, an app-facing package can have some platforms implementated within the package, and others in endorsed federated implementations.

第一步:创建 package

Step 1: Create the package

想要创建原生插件 package,请使用带有 --template=plugin 标志的 flutter create 命令。

To create a plugin package, use the --template=plugin flag with flutter create.

从 Flutter 1.20.0 版本,我们开始使用 --platforms= 这个选项,后面参数是用逗号分隔的列表,这个参数代表指定插件支持的平台。可用的平台有:androidiosweblinuxmacoswindows。如果没有指定平台,则生成的项目不支持任何平台。

As of Flutter 1.20.0, Use the --platforms= option followed by a comma separated list to specify the platforms that the plugin supports. Available platforms are: android, ios, web, linux, macos, and windows. If no platforms are specified, the resulting project doesn’t support any platforms.

使用 --org 选项,以反向域名表示法来指定你的组织。该值用于生成的 Android 及 iOS 代码。

Use the --org option to specify your organization, using reverse domain name notation. This value is used in various package and bundle identifiers in the generated plugin code.

使用 -a 选项指定 Android 的语言,或使用 -i 选项指定 iOS 的语言。请选择以下 任一项

Use the -a option to specify the language for android or the -i option to specify the language for ios. Please choose one of the following:

$ flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello
$ flutter create --org com.example --template=plugin --platforms=android,ios -a java hello
$ flutter create --org com.example --template=plugin --platforms=android,ios -i objc hello
$ flutter create --org com.example --template=plugin --platforms=android,ios -i swift hello

这将在 hello 目录下创建一个插件项目,其中包含以下内容:

This creates a plugin project in the hello folder with the following specialized content:

lib/hello.dart 文件
Dart 插件 API 实现。

lib/hello.dart
The Dart API for the plugin.

android/src/main/java/com/example/hello/HelloPlugin.kt 文件
Android 平台原生插件 API 实现(使用 Kotlin 编程语言)。

android/src/main/java/com/example/hello/HelloPlugin.kt
The Android platform-specific implementation of the plugin API in Kotlin.

ios/Classes/HelloPlugin.m 文件
iOS 平台原生插件 API 实现(使用 Objective-C 编程语言)。

ios/Classes/HelloPlugin.m
The iOS-platform specific implementation of the plugin API in Objective-C.

example/ 文件
一个依赖于该插件并说明了如何使用它的 Flutter 应用。

example/
A Flutter app that depends on the plugin, and illustrates how to use it.

默认情况下,插件项目中 iOS 代码使用 Swift 编写, Android 代码使用 Kotlin 编写。如果你更喜欢 Objective-C 或 Java,你可以通过 -i 指定 iOS 所使用的语言和/或使用-a 指定 Android 所使用的语言。比如:

By default, the plugin project uses Swift for iOS code and Kotlin for Android code. If you prefer Objective-C or Java, you can specify the iOS language using -i and the Android language using -a. For example:

$ flutter create --template=plugin --platforms=android,ios -i objc hello
$ flutter create --template=plugin --platforms=android,ios -a java hello

第二步:实现 package

Step 2: Implement the package

由于原生插件类型的 package 包含了使用多种编程语言编写的多个平台代码,因此需要一些特定步骤来保证体验的流畅性。

As a plugin package contains code for several platforms written in several programming languages, some specific steps are needed to ensure a smooth experience.

步骤 2a:定义 package API(.dart)

Step 2a: Define the package API (.dart)

原生插件类型 package 的 API 在 Dart 代码中要首先定义好,使用你钟爱的 Flutter 编辑器,打开 hello 主目录,并找到 lib/hello.dart 文件。

The API of the plugin package is defined in Dart code. Open the main hello/ folder in your favorite Flutter editor. Locate the file lib/hello.dart.

步骤 2b:添加 Android 平台代码(.kt/.java)

Step 2b: Add Android platform code (.kt/.java)

我们建议你使用 Android Studio 来编辑 Android 代码。

We recommend you edit the Android code using Android Studio.

接下来进行如下步骤:

Then use the following steps:

  1. 启动 Android Studio;

    Launch Android Studio.

  2. 在 Android Studio 的欢迎菜单 (Welcome to Android Studio) 对话框中选择打开现有的 Android Studio 项目 (Open an existing Android Studio Project),或在菜单中选择 File > Open,然后选择 hello/example/android/build.gradle 文件;

    Select Open an existing Android Studio Project in the Welcome to Android Studio dialog, or select File > Open from the menu, and select the hello/example/android/build.gradle file.

  3. Gradle Sync 对话框中,选择 OK

    In the Gradle Sync dialog, select OK.

  4. 在“Android Gradle Plugin Update”对话框中,选择“Don’t remind me again for this project”。

    In the Android Gradle Plugin Update dialog, select Don’t remind me again for this project.

插件中与 Android 系统徐相关的代码在 hello/java/com.example.hello/HelloPlugin 这个文件里。

The Android platform code of your plugin is located in hello/java/com.example.hello/HelloPlugin.

你可以在 Android Studio 中点击运行 ▶ 按钮来运行示例程序。

You can run the example app from Android Studio by pressing the run (▶) button.

步骤 2c:添加 iOS 平台代码(.swift/.h+.m)

Step 2c: Add iOS platform code (.swift/.h+.m)

我们建议你使用 Xcode 来编辑 iOS 代码。

We recommend you edit the iOS code using Xcode.

使用 Xcode 编辑 iOS 平台代码之前,首先确保代码至少被构建过一次(即从 IDE/编辑器执行示例程序,或在终端中执行以下命令: cd hello/example; flutter build ios --no-codesign)。

Before editing the iOS platform code in Xcode, first make sure that the code has been built at least once (in other words, run the example app from your IDE/editor, or in a terminal execute cd hello/example; flutter build ios --no-codesign).

接下来执行下面步骤:

Then use the following steps:

  1. 启动 Xcode

    Launch Xcode.

  2. 选择“File > Open”,然后选择 hello/example/ios/Runner.xcworkspace 文件。

    Select File > Open, and select the hello/example/ios/Runner.xcworkspace file.

插件的 iOS 平台代码位于项目导航中的这个位置: Pods/Development Pods/hello/../../example/ios/.symlinks/plugins/hello/ios/Classes

The iOS platform code for your plugin is located in Pods/Development Pods/hello/../../example/ios/.symlinks/plugins/hello/ios/Classes in the Project Navigator.

你可以点击运行 ▶ 按钮来运行示例程序。

You can run the example app by pressing the run (▶) button.

步骤 2d:关联 API 和平台代码

Step 2d: Connect the API and the platform code

最后,你需要将 Dart 编写的 API 代码与特定平台的实现相互关联。这是通过 平台通道 完成的。

Finally, you need to connect the API written in Dart code with the platform-specific implementations. This is done using a platform channel, or through the interfaces defined in a platform interface package.

为现有的插件项目加入平台的支持

Add support for platforms in an existing plugin project

要在现有的插件项目中添加对特定平台的支持,请在项目目录运行 flutter create 命令,并加入 --template=plugin。例如,要对现有的插件项目添加 Web 支持,请运行以下命令。

To add support for specific platforms to an existing plugin project, run flutter create with the --template=plugin flag again in the project directory. For example, to add web support in an existing plugin, run:

$ flutter create --template=plugin --platforms=web .

如果这个命令返回了一个关于需要更新 pubspec.yaml 文件的提醒,请按照提示的说明进行操作。

If this command displays a message about updating the pubspec.yaml file, follow the provided instructions.

仅 Dart 的平台实现

Dart-only platform implementations

如先前描述,通常插件会使用第二种语言,实现对应平台的功能。然而,在某些场景下,部分平台可能会完全使用 Dart 进行实现(例如使用 FFI)。若需要仅 Dart 的平台实现,你可以将 pubspec.yaml 里的 pluginClass 替换为 dartPluginClass。下面是 hello_windows 示例替换为仅 Dart 实现的代码:

Usually plugin implementations involve platform channels and a second language, as described above. In some cases, however, some platforms can be implemented entirely in Dart (for example, using FFI). For a Dart-only platform implementation, replace the pluginClass in pubspec.yaml with a dartPluginClass. Here is the hello_windows example above modified for a Dart-only implementation:

flutter:
  plugin:
    implements: hello
    platforms:
      windows:
        dartPluginClass: HelloPluginWindows

在这样的模式下,插件内不包含 Windows 的 C++ 代码,它将继承 hello 插件的 Dart 平台接口,使用包含静态 registerWith() 方法的 HelloPluginWindows 类进行实现。该方法会在启动时调用,用于注册 Dart 实现:

In this version you would have no C++ Windows code, and would instead subclass the hello plugin’s Dart platform interface class with a HelloPluginWindows class that includes a static registerWith() method. This method will be called during startup, and can be used to register the Dart implementation:

class HelloPluginWindows extends HelloPluginPlatform {
  /// Registers this class as the default instance of [HelloPluginPlatform].
  static void registerWith() {
    HelloPluginPlatform.instance = HelloPluginWindows();
  }

从 Flutter 2.5 版本开始,此类插件可以用于 Windows、macOS 和 Linux 插件, Android 和 iOS 在 Flutter 2.8 版本后可用。

This is supported for Windows, macOS, and Linux starting in Flutter 2.5, and for Android and iOS starting in Flutter 2.8.

测试你的插件

Testing your plugin

我们鼓励您使用自动化测试来测试您的插件,以确保代码在修改时候功能保持完整。更多信息,请参见文档 支持新的 Android 的 API 中关于 测试你的插件 这个小节。

We encourage you test your plugin with automated tests, to ensure that functionality does not regress as you make changes to your code. For more information, see Testing your plugin, a section in Supporting the new Android plugins APIs.

添加文档

Adding documentation

建议将下列文档添加到所有 package 中:

It is recommended practice to add the following documentation to all packages:

  1. README.md 文件用来对 package 进行介绍

    A README.md file that introduces the package

  2. CHANGELOG.md 文件用来记录每个版本的更改

    A CHANGELOG.md file that documents changes in each version

  3. LICENSE 文件用来阐述 package 的许可条款

    A LICENSE file containing the terms under which the package is licensed

  4. API 文档包含所有的公共 API(详情参见下文)

    API documentation for all public APIs (see below for details)

API 文档

API documentation

当你提交一个 package 时,会自动生成 API 文档并将其提交到 pub.flutter-io.cn/documentation,示例请参见 device_info 文档。

When you publish a package, API documentation is automatically generated and published to pub.dev/documentation. For example, see the docs for device_info.

如果你希望在本地开发环境中生成 API 文档,可以使用以下命令:

If you wish to generate API documentation locally on your development machine, use the following commands:

  1. 将当前工作目录切换到 package 所在目录:Change directory to the location of your package:
    cd ~/dev/mypackage
    
  2. 告知文档工具 Flutter SDK 所在位置(请自行更改 Flutter SDK 该在的位置)Tell the documentation tool where the Flutter SDK is located (change the following commands to reflect where you placed it):
       export FLUTTER_ROOT=~/dev/flutter  # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
    
       set FLUTTER_ROOT=~/dev/flutter     # on Windows (适用于 Windows 操作系统)
    
  3. 运行 `dartdoc` 工具(已经包含到 Flutter SDK 了):Run the `dartdoc` tool (included as part of the Flutter SDK), as follows:
       $FLUTTER_ROOT/bin/cache/dart-sdk/bin/dartdoc   # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
    
       %FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dartdoc  # on Windows (适用于 Windows 操作系统)
    

关于如何编写 API 文档的建议,请参阅 高效 Dart 指南

For tips on how to write API documentation, see Effective Dart Documentation.

将许可证添加到 LICENSE 文件中

Adding licenses to the LICENSE file

每个 LICENSE 文件中的各个许可证应由 80 个短线字符组成的线段进行分割。

Individual licenses inside each LICENSE file should be separated by 80 hyphens on their own on a line.

如果 LICENSE 文件中包含多个组件许可证,那么每个组件许可证必须以其所在 package 的名称开始,每个 package 名称单独一行显示,并且 package 名称列表与实际许可证内容由空行隔开。(package 名称则需与 pub package 相匹配。比如,一个 package 可能包含多个第三方代码,并且可能需要为每个 package 添加许可证。)

If a LICENSE file contains more than one component license, then each component license must start with the names of the packages to which the component license applies, with each package name on its own line, and the list of package names separated from the actual license text by a blank line. (The packages need not match the names of the pub package. For example, a package might itself contain code from multiple third-party sources, and might need to include a license for each one.)

如下是一些优秀的许可证文件:

The following example shows a well-organized license file:

package_1

<some license text>

--------------------------------------------------------------------------------
package_2

<some license text>

这些也是可以的:

Here is another example of a well-organized license file:

package_1

<some license text>

--------------------------------------------------------------------------------
package_1
package_2

<some license text>

这些是一些不太好的示例:

Here is an example of a poorly-organized license file:

<some license text>

--------------------------------------------------------------------------------
<some license text>

这也是一些不太好的示例:

Another example of a poorly-organized license file:

package_1

<some license text>
--------------------------------------------------------------------------------
<some license text>

提交 package

Publishing your package

一旦完成了 package 的实现,你便可以将其提交到 pub.dev 上,以便其他开发者可以轻松地使用它。

Once you have implemented a package, you can publish it on pub.dev, so that other developers can easily use it.

发布你的 package 之前,确保检查了这几个文件:pubspec.yamlREADME.mdCHANGELOG.md,确保它们完整且争取,另外,为了提高 package 的可用性,可以考虑加入如下的内容:

Prior to publishing, make sure to review the pubspec.yaml, README.md, and CHANGELOG.md files to make sure their content is complete and correct. Also, to improve the quality and usability of your package (and to make it more likely to achieve the status of a Flutter Favorite), consider including the following items:

接下来,运行 dry-run 命令以检验是否所有内容都通过了分析:

Next, run the publish command in dry-run mode to see if everything passes analysis:

$ flutter pub publish --dry-run

最后一步是发布,请注意:发布是永久性 的,运行以下提交命令:

The next step is publishing to pub.dev, but be sure that you are ready because publishing is forever:

$ flutter pub publish

设置了中国镜像的开发者们请注意:目前所存在的镜像都不能(也不应该)进行 package 的上传。如果你设置了镜像,执行上述发布代码可能会造成发布失败。网络设定好后,无需取消中文镜像,执行下述代码可直接上传:

$ flutter pub publish --server=https://pub.dartlang.org

有关提交的详细信息,请查阅关于 Pub 站点的 提交文档

For more details on publishing, see the publishing docs on dart.dev.

Package 依赖处理

Handling package interdependencies

如果你正在开发的 hello 依赖于另外一个 package 所公开的 Dart API,你需要将该 package 添加到文件 pubspec.yamldependencies 段中。以下代码使得插件 url_launcher 的 Dart API 在 hello 中可用:

If you are developing a package hello that depends on the Dart API exposed by another package, you need to add that package to the dependencies section of your pubspec.yaml file. The code below makes the Dart API of the url_launcher plugin available to hello:

dependencies:
  url_launcher: ^5.0.0

现在你可以在 hello 的 Dart 代码中使用 import 'package:url_launcher/url_launcher.dart'launch(someUrl)

You can now import 'package:url_launcher/url_launcher.dart' and launch(someUrl) in the Dart code of hello.

这与你在 Flutter 应用或其他任何 Dart 项目中引入 package 的方式没什么区别。

This is no different from how you include packages in Flutter apps or any other Dart project.

但碰巧 hello 是一个 原生插件 package,其特定的平台代码如果需要访问 url_launcher 所公开的平台特定 API,那么还需要为特定平台的构建文件添加适当的依赖说明,如下所示:

But if hello happens to be a plugin package whose platform-specific code needs access to the platform-specific APIs exposed by url_launcher, you also need to add suitable dependency declarations to your platform-specific build files, as shown below.

Android

hello/android/build.gradle 文件中为 url_launcher 插件设定依赖关系。

The following example sets a dependency for url_launcher in hello/android/build.gradle:

android {
    // lines skipped
    dependencies {
        compileOnly rootProject.findProject(":url_launcher")
    }
}

现在你可以在 hello/android/src 目录下的源代码文件中使用 import io.flutter.plugins.urllauncher.UrlLauncherPlugin 并访问文件 UrlLauncherPlugin

You can now import io.flutter.plugins.urllauncher.UrlLauncherPlugin and access the UrlLauncherPlugin class in the source code at hello/android/src.

如果希望了解更多有关 build.gradle 文件更多的信息,请参阅 Gradle 文档 了解构建脚本。

For more information on build.gradle files, see the Gradle Documentation on build scripts.

iOS

hello/ios/hello.podspec 文件中为 url_launcher 插件设定依赖关系。

The following example sets a dependency for url_launcher in hello/ios/hello.podspec:

Pod::Spec.new do |s|
  # lines skipped
  s.dependency 'url_launcher'

现在你可以在 hello/ios/Classes 目录下的源代码文件中使用 #import "UrlLauncherPlugin.h" 并访问 UrlLauncherPlugin 这个类了。

You can now #import "UrlLauncherPlugin.h" and access the UrlLauncherPlugin class in the source code at hello/ios/Classes.

如果希望了解更多有关 .podspec 文件更多的信息,请参阅 CocoaPods 文档 了解。

For additional details on .podspec files, see the CocoaPods Documentation on them.

Web

与其他的 Dart package 一样,所有的 Web 依赖都由文件 pubspec.yaml 来处理。

All web dependencies are handled by the pubspec.yaml file like any other Dart package.

Flutter Favorite 项目

目录

The Flutter Favorite program logo

Flutter Favorite 项目是为了在你构建应用时,能够向你提供你应该优先考虑的 package 和插件。这并不意味着它能够在你的特定情况下保证你的产品质量和适用性— 你应该始终针对你的项目情况对 packages 和插件进行自我评估。

The aim of the Flutter Favorite program is to identify packages and plugins that you should first consider when building your app. This is not a guarantee of quality or suitability to your particular situation—you should always perform your own evaluation of packages and plugins for your project.

你可以在 pub.dev 上看到完整的 Flutter Favorite packages 列表。

You can see the complete list of Flutter Favorite packages on pub.dev.

指标

Metrics

Flutter Favorite packages 通过以下指标来确认是否达到高质量标准:

Flutter Favorite packages have passed high quality standards using the following metrics:

Flutter 生态系统委员会

Flutter Ecosystem Committee

Flutter 生态系统委员会(FEC)由 Flutter 团队成员和社区成员组成并贯穿其生态系统。他们的工作之一就是确定一个 package 是否满足成为 Flutter Favorite 的质量要求。

The Flutter Ecosystem Committee (FEC) is comprised of Flutter team members and Flutter community members spread across its ecosystem. One of their jobs is to decide when a package has met the quality bar to become a Flutter Favorite.

当前的委员会成员(按姓氏字母排序)如下所示:

The current committee members (ordered alphabetically by last name) are as follows:

如果你想提名一个 package 或插件成为潜在的 Flutter Favorite,亦或是想提请其他需要引起注意的问题至委员会,请发送邮件至 委员会主席

If you’d like to nominate a package or plugin as a potential future Flutter Favorite, or would like to bring any other issues to the attention of the committee, send the committee chair an email.

Flutter Favorite 使用指南

Flutter Favorite usage guidelines

Flutter Favorite packages 会由 Flutter 团队在 pub.dev 上标注。如果你拥有一个 package 被标注未 Flutter Favorite,那么你必须遵守以下准则:

Flutter Favorite packages are labeled as such on pub.dev by the Flutter team. If you own a package that has been designated as a Flutter Favorite, you must adhere to the following guidelines:

下一步工作

What’s next

随着生态系统的不断发展,您应该期待 Flutter Favorite packages 名单会不断壮大和更新委员会将持续与 packages 作者合作以提高质量,并思考让生态系统的其他领域,如工具、咨询公司和高产的 Flutter 贡献者,也可以从 Flutter Favorite 计划中获益。

You should expect the list of Flutter Favorite packages to grow and change as the ecosystem continues to thrive. The committee will continue working with package authors to increase quality, as well as consider other areas of the ecosystem that could benefit from the Flutter Favorite program, such as tools, consulting firms, and prolific Flutter contributors.

随着 Flutter 生态系统的发展。我们将着眼于扩大指标设置,其中可能包括以下内容。

As the Flutter ecosystem grows, we’ll be looking at expanding the set of metrics, which might include the following:

Flutter Favorites

你可以在 pub.dev 上看到完整的 Flutter Favorite packages 列表。

You can see the complete list of Flutter Favorite packages on pub.dev.

后台进程

当你的应用被切换到后台时,是否仍希望它在后台可以执行一些业务逻辑?在 Flutter 里,你可以在应用被切换到后台时执行一些代码逻辑。

Have you ever wanted to execute Dart code in the background—even if your app wasn’t the currently active app? Perhaps you wanted to implement a process that watches the time, or that catches camera movement. In Flutter, you can execute Dart code in the background.

这个功能的机制主要是设置一个 isolate。isolate 是 Dart 中的多线程模型,不过其与传统线程的不同之处在于它不与主进程共享内存。你可以使用回调和回调调度器来设置 isolate,从而使应用被切换进后台时仍能执行一些业务。

The mechanism for this feature involves setting up an isolate. Isolates are Dart’s model for multithreading, though an isolate differs from a conventional thread in that it doesn’t share memory with the main program. You’ll set up your isolate for background execution using callbacks and a callback dispatcher.

有关在后台进程中使用 Dart 代码的 geofencing 案例,你可以查阅发布在 Flutter on Medium 上的一篇文章: Executing Dart in the Background with Flutter Plugins and Geofencing。在这篇文章的最后,你可以找到示例代码的链接,以及相关的 Dart、iOS 和 Android 文档。

For more information and a geofencing example that uses background execution of Dart code, see the Medium article by Ben Konyi, Executing Dart in the Background with Flutter Plugins and Geofencing. At the end of this article, you’ll find links to example code, and relevant documentation for Dart, iOS, and Android.

支持新的 Android 的 API

目录

如果您并非亲自开发或维护一个 Flutter 的 Android 插件,您可以跳过本页面。

If you don’t write or maintain an Android Flutter plugin, you can skip this page.

自 1.12 版本发布后, Android 平台已可以使用新的 Android 插件 API 。基于 PluginRegistry.Registrar 的 API 不会立刻废弃,但我们鼓励您向基于 FlutterPlugin 的 API 进行迁移。

As of the 1.12 release, new plugin APIs are available for the Android platform. The old APIs based on PluginRegistry.Registrar won’t be immediately deprecated, but we encourage you to migrate to the new APIs based on FlutterPlugin.

相较旧的 API 而言,新版 API 的优点是为生命周期的相关组件提供了更简洁清晰的访问方式。例如,在使用旧的 PluginRegistry.Registrar.activity() 时,如果 Flutter 尚未附加到任何 activites,可能会返回 null 。

The new API has the advantage of providing a cleaner set of accessors for lifecycle dependent components compared to the old APIs. For instance PluginRegistry.Registrar.activity() could return null if Flutter isn’t attached to any activities.

换句话说,在使用旧的 API 进行 Flutter 嵌入 Android 应用时,可能会产生意外的行为。 Flutter 开发团队提供的大部分 Flutter 插件 已经完成了迁移。(了解如何成为 认证的发布者)作为参考, battery package 已经迁移到新版 API 。

In other words, plugins using the old API may produce undefined behaviors when embedding Flutter into an Android app. Most of the Flutter plugins provided by the flutter.dev team have been migrated already. (Learn how to become a verified publisher on pub.dev!) For an example of a plugin that uses the new APIs, see the battery package.

升级步骤

Upgrade steps

以下的步骤简要说明了如何支持新版 API :

The following instructions outline the steps for supporting the new API:

  1. 在插件的主类文件中 (*Plugin.java) 实现 FlutterPlugin 接口。对于稍微复杂的插件,您可以将 FlutterPluginMethodCallHandler 拆分到不同的类中。如需更多关于如何使用新版 API 获取资源的内容,请参考下一节 基础插件

    同时需要注意的是,插件仍需保留静态的 registerWith() 方法,从而适配不兼容 v2 版本嵌入的应用。 (查看 Upgrading pre 1.12 Android projects 获取更多信息)

    此外,所有不可覆盖的公开成员都应该使用文档标注。在嵌入开发的场景下,这些可见内容通常需要包含文档。

    Update the main plugin class (*Plugin.java) to implement the FlutterPlugin interface. For more complex plugins, you can separate the FlutterPlugin and MethodCallHandler into two classes. See the next section, Basic plugin, for more details on accessing app resources with the latest version (v2) of embedding.

    Also, note that the plugin should still contain the static registerWith() method to remain compatible with apps that don’t use the v2 Android embedding. (See Upgrading pre 1.12 Android projects for details.) The easiest thing to do (if possible) is move the logic from registerWith() into a private method that both registerWith() and onAttachedToEngine() can call. Either registerWith() or onAttachedToEngine() will be called, not both.

    In addition, you should document all non-overridden public members within the plugin. In an add-to-app scenario, these classes are accessible to a developer and require documentation.

  2. (可选)如果您的插件需要 Activity 的引用,请同时实现 ActivityAware 接口。

    (Optional) If your plugin needs an Activity reference, also implement the ActivityAware interface.

  3. (可选)如果您的插件需要随时保持一个后台 Service ,请实现 ServiceAware 接口。

    (Optional) If your plugin is expected to be held in a background Service at any point in time, implement the ServiceAware interface.

  4. 使用 FlutterActivity 将示例应用中的 MainActivity.java 迁移到 v2 版本嵌入。更多信息请查看 Upgrading pre 1.12 Android projects 。如果您的插件类尚不存在,则必须添加一个公有构造函数。例如:

    Update the example app’s MainActivity.java to use the v2 embedding FlutterActivity. For details, see Upgrading pre 1.12 Android projects. You may have to make a public constructor for your plugin class if one didn’t exist already. For example:

     package io.flutter.plugins.firebasecoreexample;
    
     import io.flutter.embedding.android.FlutterActivity;
     import io.flutter.embedding.engine.FlutterEngine;
     import io.flutter.plugins.firebase.core.FirebaseCorePlugin;
    
     public class MainActivity extends FlutterActivity {
       // You can keep this empty class or remove it. Plugins on the new embedding
       // now automatically registers plugins.
     }
    
  5. (可选)如果您移除了 MainActivity.java,请更新 <plugin_name>/example/android/app/src/main/AndroidManifest.xml 以使用 io.flutter.embedding.android.FlutterActivity。例如:

    (Optional) If you removed MainActivity.java, update the <plugin_name>/example/android/app/src/main/AndroidManifest.xml to use io.flutter.embedding.android.FlutterActivity. For example:

      <activity android:name="io.flutter.embedding.android.FlutterActivity"
             android:theme="@style/LaunchTheme"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
             android:hardwareAccelerated="true"
             android:windowSoftInputMode="adjustResize">
             <meta-data
                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                 android:value="true" />
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
    
  6. (可选)在 MainActivity.java 同级目录下创建一个 EmbeddingV1Activity.java 文件,使用 v1 版本嵌入以持续测试您的项目对 v1 版本嵌入的兼容性。例如:

    (Optional) Create an EmbeddingV1Activity.java file] that uses the v1 embedding for the example project in the same folder as MainActivity to keep testing the v1 embedding’s compatibility with your plugin. Note that you have to manually register all the plugins instead of using GeneratedPluginRegistrant. For example:

     package io.flutter.plugins.batteryexample;
    
     import android.os.Bundle;
     import dev.flutter.plugins.e2e.E2EPlugin;
     import io.flutter.app.FlutterActivity;
     import io.flutter.plugins.battery.BatteryPlugin;
    
     public class EmbeddingV1Activity extends FlutterActivity {
       @Override
       protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         BatteryPlugin.registerWith(registrarFor("io.flutter.plugins.battery.BatteryPlugin"));
         E2EPlugin.registerWith(registrarFor("dev.flutter.plugins.e2e.E2EPlugin"));
       }
     }
    
  7. <meta-data android:name="flutterEmbedding" android:value="2"/> 添加至 <plugin_name>/example/android/app/src/main/AndroidManifest.xml 。这会让示例应用使用 v2 版本的嵌入。

    Add <meta-data android:name="flutterEmbedding" android:value="2"/> to the <plugin_name>/example/android/app/src/main/AndroidManifest.xml. This sets the example app to use the v2 embedding.

  8. (可选)如果上步您创建了 EmbeddingV1Activity ,将 EmbeddingV1Activity 添加至 <plugin_name>/example/android/app/src/main/AndroidManifest.xml 文件。例如:

    (Optional) If you created an EmbeddingV1Activity in the previous step, add the EmbeddingV1Activity to the <plugin_name>/example/android/app/src/main/AndroidManifest.xml file. For example:

     <activity
         android:name=".EmbeddingV1Activity"
         android:theme="@style/LaunchTheme"
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
         android:hardwareAccelerated="true"
         android:windowSoftInputMode="adjustResize">
     </activity>
    

测试您的插件

Testing your plugin

剩下的步骤让您可以测试您的插件,我们鼓励您这样做,但这并不是必需的。

The remaining steps address testing your plugin, which we encourage, but aren’t required.

  1. 替换 <plugin_name>/example/android/app/build.gradle 文件中 android.support.test 的引用为 androidx.test

    Update <plugin_name>/example/android/app/build.gradle to replace references to android.support.test with androidx.test:

     defaultConfig {
       ...
       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
       ...
     }
    

     dependencies {
     ...
     androidTestImplementation 'androidx.test:runner:1.2.0'
     androidTestImplementation 'androidx.test:rules:1.2.0'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
     ...
     }
    
  2. <plugin_name>/example/android/app/src/androidTest/java/<plugin_path>/ 路径下添加针对 MainActivityEmbeddingV1Activity 的测试文件,并且您需要创建该目录。例如:

    Add tests files for MainActivity and EmbeddingV1Activity in <plugin_name>/example/android/app/src/androidTest/java/<plugin_path>/. You will need to create these directories. For example:

     package io.flutter.plugins.firebase.core;
    
     import androidx.test.rule.ActivityTestRule;
     import dev.flutter.plugins.e2e.FlutterRunner;
     import io.flutter.plugins.firebasecoreexample.MainActivity;
     import org.junit.Rule;
     import org.junit.runner.RunWith;
    
     @RunWith(FlutterRunner.class)
     public class MainActivityTest {
       // Replace `MainActivity` with `io.flutter.embedding.android.FlutterActivity` if you removed `MainActivity`.
       @Rule public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
     }
    

     package io.flutter.plugins.firebase.core;
    
     import androidx.test.rule.ActivityTestRule;
     import dev.flutter.plugins.e2e.FlutterRunner;
     import io.flutter.plugins.firebasecoreexample.EmbeddingV1Activity;
     import org.junit.Rule;
     import org.junit.runner.RunWith;
    
     @RunWith(FlutterRunner.class)
     public class EmbeddingV1ActivityTest {
       @Rule
       public ActivityTestRule<EmbeddingV1Activity> rule =
           new ActivityTestRule<>(EmbeddingV1Activity.class);
     }
    
  3. <plugin_name>/pubspec.yaml<plugin_name>/example/pubspec.yaml 中的 dev_dependencies 下添加 e2eflutter_driver

    Add e2e and flutter_driver dev_dependencies to <plugin_name>/pubspec.yaml and <plugin_name>/example/pubspec.yaml.

     e2e: ^0.2.1
     flutter_driver:
       sdk: flutter
    
  4. 更新 <plugin_name>/pubspec.yaml 中 Flutter 版本的最低限制。所有已迁移的插件都将会设置最低版本为我们保证支持的最低版本 1.12.13+hotfix.6。例如:

    Update minimum Flutter version of environment in <plugin_name>/pubspec.yaml. All plugins moving forward will set the minimum version to 1.12.13+hotfix.6 which is the minimum version for which we can guarantee support. For example:

     environment:
       sdk: ">=2.0.0-dev.28.0 <3.0.0"
       flutter: ">=1.12.13+hotfix.6"
    
  5. <plugin_name>/test/<plugin_name>_e2e.dart 中创建一个简单的测试。为了测试添加了 v2 版本嵌入支持的 PR,我们将尝试测试一些插件的基础功能。这是一个确保插件正确注册到新的嵌入器的烟雾测试。例如:

    Create a simple test in <plugin_name>/test/<plugin_name>_e2e.dart. For the purpose of testing the PR that adds the v2 embedding support, we’re trying to test some very basic functionality of the plugin. This is a smoke test to ensure that the plugin properly registers with the new embedder. For example:

    import 'package:flutter_test/flutter_test.dart';
     import 'package:battery/battery.dart';
     import 'package:e2e/e2e.dart';
    
     void main() {
       E2EWidgetsFlutterBinding.ensureInitialized();
    
       testWidgets('Can get battery level', (WidgetTester tester) async {
         final Battery battery = Battery();
         final int batteryLevel = await battery.batteryLevel;
         expect(batteryLevel, isNotNull);
       });
     }
  6. 本地运行 e2e 测试。在终端中执行以下内容:

    Test run the e2e tests locally. In a terminal, do the following:

     cd <plugin_name>/example
     flutter build apk
     cd android
     ./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../../test/<plugin_name>_e2e.dart
    

基础插件

Basic plugin

要开始开发一个新的 Flutter Android 插件,请从 FlutterPlugin 的实现开始。

To get started with a Flutter Android plugin in code, start by implementing FlutterPlugin.

public class MyPlugin implements FlutterPlugin {
  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
    // TODO: your plugin is now attached to a Flutter experience.
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    // TODO: your plugin is no longer attached to a Flutter experience.
  }
}

如上述代码所示,您的插件在任意时刻都可能与 Flutter 的体验有关或无关。您需要特别注意,在 onAttachedToEngine() 进行初始化,并且在 onDetachedFromEngine() 中进行清理插件的各种引用。

As shown above, your plugin may or may not be associated with a given Flutter experience at any given moment in time. You should take care to initialize your plugin’s behavior in onAttachedToEngine(), and then cleanup your plugin’s references in onDetachedFromEngine().

FlutterPluginBinding 为您的插件提供了几个重要的引用:

The FlutterPluginBinding provides your plugin with a few important references:

binding.getFlutterEngine()
返回插件附加到的 FlutterEngine ,提供了诸如 DartExecutorFlutterRenderer 等内容的获取。

binding.getFlutterEngine()
Returns the FlutterEngine that your plugin is attached to, providing access to components like the DartExecutor, FlutterRenderer, and more.

binding.getApplicationContext()
返回当前运行的安卓应用的 Application Context

binding.getApplicationContext()
Returns the Android application’s Context for the running app.

UI/Activity 插件

UI/Activity plugin

如果您的插件需要与 UI 进行交互,例如请求权限或更改 Android UI ,那么您就需要一些附加步骤来构建您的插件。您必须实现 ActivityAware 接口。

If your plugin needs to interact with the UI, such as requesting permissions, or altering Android UI chrome, then you need to take additional steps to define your plugin. You must implement the ActivityAware interface.

public class MyPlugin implements FlutterPlugin, ActivityAware {
  //...normal plugin behavior is hidden...

  @Override
  public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
    // TODO: your plugin is now attached to an Activity
  }

  @Override
  public void onDetachedFromActivityForConfigChanges() {
    // TODO: the Activity your plugin was attached to was
    // destroyed to change configuration.
    // This call will be followed by onReattachedToActivityForConfigChanges().
  }

  @Override
  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
    // TODO: your plugin is now attached to a new Activity
    // after a configuration change.
  }

  @Override
  public void onDetachedFromActivity() {
    // TODO: your plugin is no longer associated with an Activity.
    // Clean up references.
  }
}

若需要与 Activity 交互,您已经实现 ActivityAware 的插件需要在 4 个不同的阶段实现不同的行为。首先,确保您的插件已经附加至 Activity 。您可以通过提供的 ActivityPluginBinding 获取到 Activity 及一些回调。

To interact with an Activity, your ActivityAware plugin must implement appropriate behavior at 4 stages. First, your plugin is attached to an Activity. You can access that Activity and a number of its callbacks through the provided ActivityPluginBinding.

由于 Activity 有可能在配置变化时被销毁,您必须在 onDetachedFromActivityForConfigChanges() 方法中清理所有与 Activity 有关的引用,接着在 onReattachedToActivityForConfigChanges() 中重新建立它们。

Since Activitys can be destroyed during configuration changes, you must cleanup any references to the given Activity in onDetachedFromActivityForConfigChanges(), and then re-establish those references in onReattachedToActivityForConfigChanges().

最后,在 onDetachedFromActivity() 中清理所有与 Activity 有关的引用并返回与 UI 无关的配置。

Finally, in onDetachedFromActivity() your plugin should clean up all references related to Activity behavior and return to a non-UI configuration.

将 Flutter 集成到现有应用

目录

集成到现有应用

Add-to-app

有时候,用 Flutter 一次性重写整个已有的应用是不切实际的。对于这些情况,Flutter 可以作为一个库或模块,集成进现有的应用当中。模块引入到您的 Android 或 iOS 应用(当前支持的平台)中,以使用 Flutter 来渲染一部分的 UI,或者仅运行多平台共享的 Dart 代码逻辑。

It’s sometimes not practical to rewrite your entire application in Flutter all at once. For those situations, Flutter can be integrated into your existing application piecemeal, as a library or module. That module can then be imported into your Android or iOS (currently supported platforms) app to render a part of your app’s UI in Flutter. Or, just to run shared Dart logic.

仅需几步,你就可以将高效而富有表现力的 Flutter 引入您的应用。

In a few steps, you can bring the productivity and the expressiveness of Flutter into your own app.

在 Flutter v1.12 中,添加到现有应用的基本场景已被支持,每个应用在同一时间可以集成一个全屏幕的 Flutter 实例。目前仍有以下限制:

As of Flutter v1.12, add-to-app is supported for the basic scenario of integrating one full-screen Flutter instance at a time per app. It currently has the following limitations:

自 Flutter 1.26 版本开始,add-to-app 开始实验性的支持将多个 Flutter 引擎 (engine)、页面 (screen) 或视图 (view) 添加到你的应用中。适用于混合栈应用在导航到原生页面和 Flutter 页面的情况,也适用于一个页面有原生视图和 Flutter 视图的情况等混合栈应用。多个 Flutter 实例会帮助每个实例保持独立的应用和 UI 状态,同时使用最少的内存资源。请多详细内容,请参考文档: 多个 Flutter 实例

As of Flutter v1.26, add-to-app experimentally supports adding multiple instances of Flutter engines, screens, or views into your app. This can help integration scenarios such as a hybrid navigation stack with mixed native and Flutter screens, or a page with multiple partial-screen Flutter views. Having multiple Flutter instances allows each instance to maintain independent application and UI state while using minimal memory resources. See more in the multiple Flutters page.

已支持的特性

Supported features

集成到 Android 应用

Add to Android applications

Add-to-app steps on Android

集成到 iOS 应用

Add to iOS applications

Add-to-app steps on iOS

查看 add-to-app GitHub 示例仓库 中在 iOS 和 Android 平台上引入 Flutter module 的示例项目。

See our add-to-app GitHub Samples repository for sample projects in Android and iOS that import a Flutter module for UI.

开始

Get started

第一步,查看以下工程集成指南

To get started, see our project integration guide for Android and iOS:

Android
iOS

API 用法

API usage

将 Flutter 集成进您的工程后,可以查看以下 API 使用指南

After Flutter is integrated into your project, see our API usage guides at the following links:

Android
iOS

将 Flutter module 集成到 Android 项目

目录

Flutter 可以作为 Gradle 子项目源码或者 AAR 嵌入到现有的 Android 应用程序中。

Flutter can be embedded into your existing Android application piecemeal, as a source code Gradle subproject or as AARs.

开发者可以使用带有 Flutter 插件 的 Android Studio 或手动完成整个集成流程。

The integration flow can be done using the Android Studio IDE with the Flutter plugin or manually.

使用 Android Studio

Using Android Studio

直接使用 Android Studio 是在现有应用中自动集成 Flutter 模块比较便捷的方法。在 Android Studio 中,你可以在一个项目中同时编写 Android 代码和 Flutter 代码,还可以继续使用各种常用的 IntelliJ Flutter 插件功能,例如 Dart 代码自动补全、热重载和 widget 检查器等。

The Android Studio IDE is a convenient way of integrating your Flutter module automatically. With Android Studio, you can co-edit both your Android code and your Flutter code in the same project. You can also continue to use your normal IntelliJ Flutter plugin functionalities such as Dart code completion, hot reload, and widget inspector.

只有在 Android Studio 3.6 及以上的版本,配合 42 以上版本的 IntelliJ Flutter 插件 才能直接通过 Android Studio 执行集成流程,并且,Android Studio 目前仅支持以 Gradle 子项目源码的方式集成,而不能以 AAR 方式集成。有关这两种方式的区别及更多详细信息,请参见下文。

Add-to-app flows with Android Studio are only supported on Android Studio 3.6 with version 42+ of the Flutter plugin for IntelliJ. The Android Studio integration also only supports integrating using a source code Gradle subproject, rather than using AARs. See below for more details on the distinction.

在 Android Studio 打开现有的 Android 项目并点击菜单按钮 File > New > New Module… ,这样就可以创建出一个可以集成的新 Flutter 模块,或者选择导入已有的 Flutter 模块。

Using the File > New > New Module… menu in Android Studio in your existing Android project, you can either create a new Flutter module to integrate, or select an existing Flutter module that was created previously.

如果你想创建一个新的 Flutter 模块,则可以直接在向导窗口中填写模块名称、路径等信息。

If you create a new module, you can use a wizard to select the module name, location, and so on.

此时,Android Studio 插件就会自动为这个 Android 项目配置添加 Flutter 模块作为依赖项,这时集成应用就已准备好进行下一步的构建。

The Android Studio plugin automatically configures your Android project to add your Flutter module as a dependency, and your app is ready to build.

现在,应用程序已经包含了 Flutter 模块作为依赖项,你可以跳转至 向 Android 应用中添加 Flutter 页面 执行后续步骤。

Your app now includes the Flutter module as a dependency. You can jump to the Adding a Flutter screen to an Android app to follow the next steps.

手动集成

Manual integration

如果想要在不使用 Flutter 的 Android Studio 插件的情况下手动将 Flutter 模块与现有的 Android 应用集成,可以参考以下步骤:

To integrate a Flutter module with an existing Android app manually, without using Flutter’s Android Studio plugin, follow these steps:

创建 Flutter 模块

Create a Flutter module

假设你在 some/path/MyApp 路径下已有一个 Android 应用,并且你希望 Flutter 项目作为同级项目:

Let’s assume that you have an existing Android app at some/path/MyApp, and that you want your Flutter project as a sibling:

$ cd some/path/
$ flutter create -t module --org com.example my_flutter

这会创建一个 some/path/my_flutter/ 的 Flutter 模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

This creates a some/path/my_flutter/ Flutter module project with some Dart code to get you started and an .android/ hidden subfolder. The .android folder contains an Android project that can both help you run a barebones standalone version of your Flutter module via flutter run and it’s also a wrapper that helps bootstrap the Flutter module an embeddable Android library.

引入 Java 8

Java 8 requirement

Flutter Android 引擎需要使用到 Java 8 中的新特性。

The Flutter Android engine uses Java 8 features.

在尝试将 Flutter 模块项目集成到宿主 Android 应用之前,请先确保宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性,例如:

Before attempting to connect your Flutter module project to your host Android app, ensure that your host Android app declares the following source compatibility within your app’s build.gradle file, under the android { } block, such as:

android {
  //...
  compileOptions {
    sourceCompatibility 1.8
    targetCompatibility 1.8
  }
}

将 Flutter module 作为依赖项

Add the Flutter module as a dependency

接下来,将 Flutter 模块添加为 Gradle 中宿主应用程序的依赖项。主要有两种方法实现。 AAR 机制可以为每个 Flutter 模块创建 Android AAR 作为依赖媒介。当你的宿主应用程序开发者不想安装 Flutter SDK 时,这是一个很好方案,但是,如果你想要经常编译,那么每次都需要重新编译一次,该步骤不可避免。

Next, add the Flutter module as a dependency of your existing app in Gradle. There are two ways to achieve this. The AAR mechanism creates generic Android AARs as intermediaries that packages your Flutter module. This is good when your downstream app builders don’t want to have the Flutter SDK installed. But, it adds one more build step if you build frequently.

直接将 Flutter 模块的源码作为子项目的依赖机制是一种便捷的一键式构建方案,但此时需要另外安装 Flutter SDK,这是目前 Android Studio IDE 插件使用的机制。

The source code subproject mechanism is a convenient one-click build process, but requires the Flutter SDK. This is the mechanism used by the Android Studio IDE plugin.

方案 A - 依赖 Android Archive (AAR)

Option A - Depend on the Android Archive (AAR)

这种方式会将 Flutter 库打包成由 AAR 和 POM artifacts 组成的本地 Maven 存储库。这种方案可以使你的团队不需要安装 Flutter SDK 即可编译宿主应用。之后,你可以从本地或远程存储库中分发更新 artifacts。

This option packages your Flutter library as a generic local Maven repository composed of AARs and POMs artifacts. This option allows your team to build the host app without installing the Flutter SDK. You can then distribute the artifacts from a local or remote repository.

假设你在 some/path/my_flutter 下构建 Flutter 模块,执行如下命令:

Let’s assume you built a Flutter module at some/path/my_flutter, and then run:

$ cd some/path/my_flutter
$ flutter build aar

然后,根据屏幕上的提示完成集成操作。

Then, follow the on-screen instructions to integrate.

详细地说,该命令应用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件:

More specifically, this command creates (by default all debug/profile/release modes) a local repository, with the following files:

build/host/outputs/repo
└── com
    └── example
        └── my_flutter
            ├── flutter_release
            │   ├── 1.0
            │   │   ├── flutter_release-1.0.aar
            │   │   ├── flutter_release-1.0.aar.md5
            │   │   ├── flutter_release-1.0.aar.sha1
            │   │   ├── flutter_release-1.0.pom
            │   │   ├── flutter_release-1.0.pom.md5
            │   │   └── flutter_release-1.0.pom.sha1
            │   ├── maven-metadata.xml
            │   ├── maven-metadata.xml.md5
            │   └── maven-metadata.xml.sha1
            ├── flutter_profile
            │   ├── ...
            └── flutter_debug
                └── ...

要依赖 AAR,宿主应用必须能够找到这些文件。

To depend on the AAR, the host app must be able to find these files.

为此,需要在宿主应用程序中修改 app/build.gradle 文件,使其包含本地存储库和上述依赖项:

To do that, edit app/build.gradle in your host app so that it includes the local repository and the dependency:

android {
  // ...
}

repositories {
  maven {
    url 'some/path/my_flutter/build/host/outputs/repo'
    // This is relative to the location of the build.gradle file
    // if using a relative path.
  }
  maven {
    url 'https://storage.googleapis.com/download.flutter.io'
  }
}

dependencies {
  // ...
  debugImplementation 'com.example.flutter_module:flutter_debug:1.0'
  profileImplementation 'com.example.flutter_module:flutter_profile:1.0'
  releaseImplementation 'com.example.flutter_module:flutter_release:1.0'
}

你的应用程序现在添加了 Flutter 模块作为依赖项,下面,你可以按照 向 Android 应用中添加 Flutter 页面 中的后续步骤继续操作。

Your app now includes the Flutter module as a dependency. You can follow the next steps in the Adding a Flutter screen to an Android app.

方案 B - 依赖模块的源码

Option B - Depend on the module’s source code

该方式可以使你的 Android 项目和 Flutter 项目能够同步一键式构建。当你需要同时在这两个项目中进行快速迭代时,这种方案非常方便,但是此时,你的团队必须安装 Flutter SDK 才能构建宿主应用程序。

This option enables a one-step build for both your Android project and Flutter project. This option is convenient when you work on both parts simultaneously and rapidly iterate, but your team must install the Flutter SDK to build the host app.

将 Flutter 模块作为子项目添加到宿主应用的 settings.gradle 中:

Include the Flutter module as a subproject in the host app’s settings.gradle:

// Include the host app project.
include ':app'                                    // assumed existing content
setBinding(new Binding([gradle: this]))                                // new
evaluate(new File(                                                     // new
  settingsDir.parentFile,                                              // new
  'my_flutter/.android/include_flutter.groovy'                         // new
))                                                                     // new

假设 my_flutterMyApp 是同级目录。

Assuming my_flutter is a sibling to MyApp.

binding 和 evaluation 脚本可以使 Flutter 模块将其自身(如 :flutter)和该模块使用的所有 Flutter 插件(如 :package_info:video_player 等)都包含在 settings.gradle 的评估的上下文中。

The binding and script evaluation allows the Flutter module to include itself (as :flutter) and any Flutter plugins used by the module (as :package_info, :video_player, etc) in the evaluation context of your settings.gradle.

在你的应用中引入对 Flutter 模块的依赖:

Introduce an implementation dependency on the Flutter module from your app:

dependencies {
  implementation project(':flutter')
}

此时,你的应用程序已将 Flutter 模块添加为依赖项,下面,你可以按照 向 Android 应用中添加 Flutter 页面 中的后续步骤继续操作。

Your app now includes the Flutter module as a dependency. You can follow the next steps in the Adding a Flutter screen to an Android app.

在 Android 应用中添加 Flutter 页面

目录

本指南讲述了如何在一个现有的 Android 应用中添加单个 Flutter 页面。添加到应用中的单个 Flutter 页面可以是不透明的普通页面,也可以是透明的页面。这两种页面的使用都会在本指南中提到。

This guide describes how to add a single Flutter screen to an existing Android app. A Flutter screen can be added as a normal, opaque screen, or as a see-through, translucent screen. Both options are described in this guide.

添加一个普通的 Flutter 页面

Add a normal Flutter screen

Add Flutter Screen Header

步骤 1:在 AndroidManifest.xml 中添加 FlutterActivity

Step 1: Add FlutterActivity to AndroidManifest.xml

Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内:

Flutter provides FlutterActivity to display a Flutter experience within an Android app. Like any other Activity, FlutterActivity must be registered in your AndroidManifest.xml. Add the following XML to your AndroidManifest.xml file under your application tag:

<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

上述代码中的 @style/LaunchTheme 可以替换为想要在你的 FlutterActivity 中使用的其他 Android 主题。主题的选择决定 Android 系统展示框架所使用的颜色,例如 Android 的导航栏,以及 Flutter UI 自身的第一次渲染前 FlutterActivity 的背景色。

The reference to @style/LaunchTheme can be replaced by any Android theme that want to apply to your FlutterActivity. The choice of theme dictates the colors applied to Android’s system chrome, like Android’s navigation bar, and to the background color of the FlutterActivity just before the Flutter UI renders itself for the first time.

步骤 2:加载 FlutterActivity

Step 2: Launch FlutterActivity

在你的清单文件中注册了 FlutterActivity 之后,根据需要,你可以在应用中的任意位置添加打开 FlutterActivity 的代码。下边的代码展示了如何在 OnClickListener 的点击事件中打开 FlutterActivity

With FlutterActivity registered in your manifest file, add code to launch FlutterActivity from whatever point in your app that you’d like. The following example shows FlutterActivity being launched from an OnClickListener.

myButton.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity.createDefaultIntent(currentActivity)
    );
  }
});
myButton.setOnClickListener {
  startActivity(
    FlutterActivity.createDefaultIntent(this)
  )
}

上述的例子假定了你的 Dart 代码入口是调用 main(),并且你的 Flutter 初始路由是 ‘/’。 Dart 代码入口不能通过 Intent 改变,但是初始路由可以通过 Intent 来修改。下面的例子讲解了如何打开一个自定义 Flutter 初始路由的 FlutterActivity

The previous example assumes that your Dart entrypoint is called main(), and your initial Flutter route is ‘/’. The Dart entrypoint can’t be changed using Intent, but the initial route can be changed using Intent. The following example demonstrates how to launch a FlutterActivity that initially renders a custom route in Flutter.

myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withNewEngine()
        .initialRoute("/my_route")
        .build(currentActivity)
      );
  }
});
myButton.setOnClickListener {
  startActivity(
    FlutterActivity
      .withNewEngine()
      .initialRoute("/my_route")
      .build(this)
  )
}

可以用你想要的初始路由替换掉 "/my_route"

Replace "/my_route" with your desired initial route.

工厂方法 withNewEngine() 可以用于配置一个 FlutterActivity,它会在内部创建一个属于自己的 FlutterEngine 实例,这会有一个明显的初始化时间。另外一种可选的做法是让 FlutterActivity 使用一个预热且缓存的 FlutterEngine,这可以最小化 Flutter 初始化的时间。这种方式接下来会讨论到。

The use of the withNewEngine() factory method configures a FlutterActivity that internally create its own FlutterEngine instance. This comes with a non-trivial initialization time. The alternative approach is to instruct FlutterActivity to use a pre-warmed, cached FlutterEngine, which minimizes Flutter’s initialization time. That approach is discussed next.

步骤 3:(可选)使用缓存的 FlutterEngine

Step 3: (Optional) Use a cached FlutterEngine

每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine

Every FlutterActivity creates its own FlutterEngine by default. Each FlutterEngine has a non-trivial warm-up time. This means that launching a standard FlutterActivity comes with a brief delay before your Flutter experience becomes visible. To minimize this delay, you can warm up a FlutterEngine before arriving at your FlutterActivity, and then you can use your pre-warmed FlutterEngine instead.

要预热一个 FlutterEngine,可以在你的应用中找一个合理的地方实例化一个 FlutterEngine。下面的这个例子是在 Application 类中预先初始化一个 FlutterEngine

To pre-warm a FlutterEngine, find a reasonable location in your app to instantiate a FlutterEngine. The following example arbitrarily pre-warms a FlutterEngine in the Application class:

public class MyApplication extends Application {
  public FlutterEngine flutterEngine;
  
  @Override
  public void onCreate() {
    super.onCreate();
    // Instantiate a FlutterEngine.
    flutterEngine = new FlutterEngine(this);

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );

    // Cache the FlutterEngine to be used by FlutterActivity.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine

  override fun onCreate() {
    super.onCreate()

    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)

    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )

    // Cache the FlutterEngine to be used by FlutterActivity.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}

传给 FlutterEngineCache 的 ID 可以是你想要的任何名称。确保 FlutterActivityFlutterFragment 在使用缓存的 FlutterEngine 时,传递了同样的 ID。基于缓存的 FlutterEngine 来使用 FlutterActivity 会在后续讨论到。

The ID passed to the FlutterEngineCache can be whatever you want. Make sure that you pass the same ID to any FlutterActivity or FlutterFragment that should use the cached FlutterEngine. Using FlutterActivity with a cached FlutterEngine is discussed next.

要使用预热且缓存的 FlutterEngine 时,让你的 FlutterActivity 从缓存中获取 FlutterEngine,而不是创建一个新的。可以使用 FlutterActivitywithCachedEngine() 方法来实现:

With a pre-warmed, cached FlutterEngine, you now need to instruct your FlutterActivity to use the cached FlutterEngine instead of creating a new one. To accomplish this, use FlutterActivity’s withCachedEngine() builder:

myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withCachedEngine("my_engine_id")
        .build(currentActivity)
      );
  }
});
myButton.setOnClickListener {
  startActivity(
    FlutterActivity
      .withCachedEngine("my_engine_id")
      .build(this)
  )
}

当使用 withCachedEngine() 方法时,传递你缓存对应 FlutterEngine 时用的相同 ID。

When using the withCachedEngine() factory method, pass the same ID that you used when caching the desired FlutterEngine.

现在,当你加载 FlutterActivity 时,在展示 Flutter 内容前的延迟会明显降低。

Now, when you launch FlutterActivity, there is significantly less delay in the display of Flutter content.

Initial route with a cached engine

当配置一个使用新 FlutterEngineFlutterActivity 或者 FlutterFragment 时,会使用到初始路由的概念。但是,使用缓存中的 Flutter 引擎时, FlutterActivity 或者 FlutterFragment 则没有涉及初始路由的概念。这是因为被缓存的引擎理论上已经执行了 Dart 代码,在这时配置初始路由已经太迟了。

The concept of an initial route is available when configuring a FlutterActivity or a FlutterFragment with a new FlutterEngine. However, FlutterActivity and FlutterFragment don’t offer the concept of an initial route when using a cached engine. This is because a cached engine is expected to already be running Dart code, which means it’s too late to configure the initial route.

开发者如果想要让缓存中的引擎从自定义的初始路由开始运行,那么可以执行 Dart 入口前,为缓存的 FlutterEngine 配置自定义的初始路由。如下面这个例子:

Developers that would like their cached engine to begin with a custom initial route can configure their cached FlutterEngine to use a custom initial route just before executing the Dart entrypoint. The following example demonstrates the use of an initial route with a cached engine:

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // Instantiate a FlutterEngine.
    flutterEngine = new FlutterEngine(this);
    // Configure an initial route.
    flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine
  override fun onCreate() {
    super.onCreate()
    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)
    // Configure an initial route.
    flutterEngine.navigationChannel.setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}

通过设置导航通道中的初始路由,会让关联的 FlutterEnginerunApp() 方法首次执行后,展示已配置的路由页面。

By setting the initial route of the navigation channel, the associated FlutterEngine displays the desired route upon initial execution of the runApp() Dart function.

runApp() 的首次执行之后,修改导航通道中的初始路由属性是不会生效的。想要在不同的 ActivityFragment 之间使用同一个 FlutterEngine,并且在其展示时切换不同的路由,开发者需要设置一个方法通道,来显式地通知他们的 Dart 代码切换 Navigator 路由。

Changing the initial route property of the navigation channel after the initial execution of runApp() has no effect. Developers who would like to use the same FlutterEngine between different Activitys and Fragments and switch the route between those displays need to setup a method channel and explicitly instruct their Dart code to change Navigator routes.

Add a translucent Flutter screen

Add Flutter Screen With Translucency Header

大部分的全屏 Flutter 交互页面是不透明的。但是,一些应用可能会发布一个类似模态框的 Flutter 页面,例如,一个对话框或者底部工作表。 Flutter 默认支持透明的 FlutterActivity

Most full-screen Flutter experiences are opaque. However, some apps would like to deploy a Flutter screen that looks like a modal, for example, a dialog or bottom sheet. Flutter supports translucent FlutterActivitys out of the box.

要将你的 FlutterActivity 设置为透明,在创建和加载 FlutterActivity 的常规步骤中做如下的变更。

To make your FlutterActivity translucent, make the following changes to the regular process of creating and launching a FlutterActivity.

步骤 1:使用透明的主题

Step 1: Use a theme with translucency

Android 需要一个特殊的主题属性来让 Activity 以一个透明的背景渲染。使用如下属性来创建或者修改一个 Android 主题:

Android requires a special theme property for Activitys that render with a translucent background. Create or update an Android theme with the following property:

<style name="MyTheme" parent="@style/MyParentTheme">
  <item name="android:windowIsTranslucent">true</item>
</style>

然后,将透明主题应用到你的 FlutterActivity

Then, apply the translucent theme to your FlutterActivity.

<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/MyTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

现在你的 FlutterActivity 已经支持透明化。下一步,你需要在打开 FlutterActivity 时显式启用透明模式。

Your FlutterActivity now supports translucency. Next, you need to launch your FlutterActivity with explicit transparency support.

步骤 2:启动透明的 FlutterActivity

Step 2: Start FlutterActivity with transparency

要打开透明背景的 FlutterActivity,需要把对应的 BackgroundMode 传递给 IntentBuilder

To launch your FlutterActivity with a transparent background, pass the appropriate BackgroundMode to the IntentBuilder:

// Using a new FlutterEngine.
startActivity(
  FlutterActivity
    .withNewEngine()
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);

// Using a cached FlutterEngine.
startActivity(
  FlutterActivity
    .withCachedEngine("my_engine_id")
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);
// Using a new FlutterEngine.
startActivity(
  FlutterActivity
    .withNewEngine()
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(this)
);

// Using a cached FlutterEngine.
startActivity(
  FlutterActivity
    .withCachedEngine("my_engine_id")
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(this)
);

现在你的 FlutterAcivity 的背景已经是透明的了。

You now have a FlutterActivity with a transparent background.

向 Android 应用中添加 Flutter Fragment

目录

Add Flutter Fragment Header

本篇指南介绍如何向一个现有的 Android 应用中添加 Flutter Fragment。在 Android 开发中,一个 Fragment 代表了一块较大的模块化 UI。 Fragment 可能被用来展示滑动抽屉、标签内容和 ViewPager 中的页面,或者在单 Activity 应用中,Fragment 可能仅代表正常的屏幕内容。 Flutter 提供了FlutterFragment,以便于开发者们可以在任何使用常规 Fragment 的地方呈现 Flutter 的内容。

This guide describes how to add a Flutter Fragment to an existing Android app. In Android, a Fragment represents a modular piece of a larger UI. A Fragment might be used to present a sliding drawer, tabbed content, a page in a ViewPager, or it might simply represent a normal screen in a single-Activity app. Flutter provides a FlutterFragment so that developers can present a Flutter experience any place that they can use a regular Fragment.

如果 Activity 同样适用于您的应用需求,可以考虑 使用 FlutterActivity 而非 FlutterFragment,前者更加快捷易用。

If an Activity is equally applicable for your application needs, consider using a FlutterActivity instead of a FlutterFragment, which is quicker and easier to use.

FlutterFragment 允许开发者在 Fragment 中控制以下 Flutter 的开发细节:

FlutterFragment allows developers to control the following details of the Flutter experience within the Fragment:

FlutterFragment 还提供了一些回调事件,这些回调必须由它所在的 Activity 触发执行。这些回调允许 Flutter 适时地响应一些系统事件。

FlutterFragment also comes with a number of calls that must be forwarded from its surrounding Activity. These calls allow Flutter to react appropriately to OS events.

这篇指南介绍了 FlutterFragment 的所有使用方式和使用要求。

All varieties of FlutterFragment, and its requirements, are described in this guide.

使用新的 FlutterEngineActivity 中添加 FlutterFragment

Add a FlutterFragment to an Activity with a new FlutterEngine

使用 FlutterFragment 的第一步是将其添加进宿主 Activity

The first thing to do to use a FlutterFragment is to add it to a host Activity.

要向宿主 Activity 中添加 FlutterFragment,需要在 ActivityonCreate() 或者其它合适的地方,实例化 FlutterFragment 并且与 Activity 绑定。

To add a FlutterFragment to a host Activity, instantiate and attach an instance of FlutterFragment in onCreate() within the Activity, or at another time that works for your app:

public class MyActivity extends FragmentActivity {
    // Define a tag String to represent the FlutterFragment within this
    // Activity's FragmentManager. This value can be whatever you'd like.
    private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";

    // Declare a local variable to reference the FlutterFragment so that you
    // can forward calls to it later.
    private FlutterFragment flutterFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Inflate a layout that has a container for your FlutterFragment.
        // For this example, assume that a FrameLayout exists with an ID of
        // R.id.fragment_container.
        setContentView(R.layout.my_activity_layout);

        // Get a reference to the Activity's FragmentManager to add a new
        // FlutterFragment, or find an existing one.
        FragmentManager fragmentManager = getSupportFragmentManager();

        // Attempt to find an existing FlutterFragment,
        // in case this is not the first time that onCreate() was run.
        flutterFragment = (FlutterFragment) fragmentManager
            .findFragmentByTag(TAG_FLUTTER_FRAGMENT);

        // Create and attach a FlutterFragment if one does not exist.
        if (flutterFragment == null) {
            flutterFragment = FlutterFragment.createDefault();

            fragmentManager
                .beginTransaction()
                .add(
                    R.id.fragment_container,
                    flutterFragment,
                    TAG_FLUTTER_FRAGMENT
                )
                .commit();
        }
    }
}
class MyActivity : FragmentActivity() {
  companion object {
    // Define a tag String to represent the FlutterFragment within this
    // Activity's FragmentManager. This value can be whatever you'd like.
    private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
  }

  // Declare a local variable to reference the FlutterFragment so that you
  // can forward calls to it later.
  private var flutterFragment: FlutterFragment? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Inflate a layout that has a container for your FlutterFragment. For
    // this example, assume that a FrameLayout exists with an ID of
    // R.id.fragment_container.
    setContentView(R.layout.my_activity_layout)

    // Get a reference to the Activity's FragmentManager to add a new
    // FlutterFragment, or find an existing one.
    val fragmentManager: FragmentManager = supportFragmentManager

    // Attempt to find an existing FlutterFragment, in case this is not the
    // first time that onCreate() was run.
    flutterFragment = fragmentManager
      .findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?

    // Create and attach a FlutterFragment if one does not exist.
    if (flutterFragment == null) {
      var newFlutterFragment = FlutterFragment.createDefault()
      flutterFragment = newFlutterFragment
      fragmentManager
        .beginTransaction()
        .add(
          R.id.fragment_container,
          newFlutterFragment,
          TAG_FLUTTER_FRAGMENT
        )
        .commit()
    }
  }
}

上面的代码会以 main() 为 Dart 入口函数, / 为初始路由,并使用新的 FlutterEngine,能够正确渲染出 Flutter UI。但是,这些代码还无法使 Flutter 如预期一样完全正常地工作。 Flutter 依赖操作系统的各种信号,这些信号必须通过宿主 Activity 发送到 FlutterFragment 中。下面的示例展示了这些系统回调:

The previous code is sufficient to render a Flutter UI that begins with a call to your main() Dart entrypoint, an initial Flutter route of /, and a new FlutterEngine. However, this code is not sufficient to achieve all expected Flutter behavior. Flutter depends on various OS signals that must be forwarded from your host Activity to FlutterFragment. These calls are shown in the following example:

public class MyActivity extends FragmentActivity {
    @Override
    public void onPostResume() {
        super.onPostResume();
        flutterFragment.onPostResume();
    }

    @Override
    protected void onNewIntent(@NonNull Intent intent) {
        flutterFragment.onNewIntent(intent);
    }

    @Override
    public void onBackPressed() {
        flutterFragment.onBackPressed();
    }

    @Override
    public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults
    ) {
        flutterFragment.onRequestPermissionsResult(
            requestCode,
            permissions,
            grantResults
        );
    }

    @Override
    public void onUserLeaveHint() {
        flutterFragment.onUserLeaveHint();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        flutterFragment.onTrimMemory(level);
    }
}
class MyActivity : FragmentActivity() {
  override fun onPostResume() {
    super.onPostResume()
    flutterFragment!!.onPostResume()
  }

  override fun onNewIntent(@NonNull intent: Intent) {
    flutterFragment!!.onNewIntent(intent)
  }

  override fun onBackPressed() {
    flutterFragment!!.onBackPressed()
  }

  override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String?>,
    grantResults: IntArray
  ) {
    flutterFragment!!.onRequestPermissionsResult(
      requestCode,
      permissions,
      grantResults
    )
  }

  override fun onUserLeaveHint() {
    flutterFragment!!.onUserLeaveHint()
  }

  override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    flutterFragment!!.onTrimMemory(level)
  }
}

随着 OS 信号传递到 Flutter,您的 FlutterFragment 可以如预期正常工作。现在可以尝试将 FlutterFragment 添加进您的 Android 应用了。

With the OS signals forwarded to Flutter, your FlutterFragment works as expected. You have now added a FlutterFragment to your existing Android app.

使用新的 FlutterEngine 是最简单的集成方式,但是会存在一段明显的初始化时间,此时,在 Flutter 初始化和首次渲染完成之前会出现短暂的白屏。使用缓存、预热的 FlutterEngine 则可以避免上述的大部分耗时,下面我们将讨论这些内容。

The simplest integration path uses a new FlutterEngine, which comes with a non-trivial initialization time, leading to a blank UI until Flutter is initialized and rendered the first time. Most of this time overhead can be avoided by using a cached, pre-warmed FlutterEngine, which is discussed next.

使用预热的 FlutterEngine

Using a pre-warmed FlutterEngine

默认情况下,FlutterFragment 会创建它自己的 FlutterEngine 实例,同时也需要不少的启动时间。这就意味着您的用户会看到短暂的白屏。通过使用已存在的、预热的 FlutterEngine 就可以大幅度减少启动的耗时。

By default, a FlutterFragment creates its own instance of a FlutterEngine, which requires non-trivial warm-up time. This means your user sees a blank Fragment for a brief moment. You can mitigate most of this warm-up time by using an existing, pre-warmed instance of FlutterEngine.

要在 FlutterFragment 中使用预热 FlutterEngine,可以使用工厂方法 withCachedEngine() 实例化 FlutterFragment

To use a pre-warmed FlutterEngine in a FlutterFragment, instantiate a FlutterFragment with the withCachedEngine() factory method.

// Somewhere in your app, before your FlutterFragment is needed,
// like in the Application class ...
// Instantiate a FlutterEngine.
FlutterEngine flutterEngine = new FlutterEngine(context);

// Start executing Dart code in the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
);

// Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
FlutterEngineCache
  .getInstance()
  .put("my_engine_id", flutterEngine);
FlutterFragment.withCachedEngine("my_engine_id").build();
// Somewhere in your app, before your FlutterFragment is needed,
// like in the Application class ...
// Instantiate a FlutterEngine.
val flutterEngine = FlutterEngine(context)

// Start executing Dart code in the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartEntrypoint.createDefault()
)

// Cache the pre-warmed FlutterEngine to be used later by FlutterFragment.
FlutterEngineCache
  .getInstance()
  .put("my_engine_id", flutterEngine)
FlutterFragment.withCachedEngine("my_engine_id").build()

FlutterFragment 内部可访问 FlutterEngineCache,并且可以根据传递给 withCachedEngine() 的 ID 获取预热的 FlutterEngine

FlutterFragment internally knows about FlutterEngineCache and retrieves the pre-warmed FlutterEngine based on the ID given to withCachedEngine().

如上所示,通过提供预热的 FlutterEngine,您的应用将以最快速度渲染出第一帧。

By providing a pre-warmed FlutterEngine, as previously shown, your app renders the first Flutter frame as quickly as possible.

缓存引擎中的初始路由

Initial route with a cached engine

当配置一个使用新 FlutterEngineFlutterActivity 或者 FlutterFragment 时,会使用到初始路由的概念。但是,使用缓存中的 Flutter 引擎时, FlutterActivity 或者 FlutterFragment 则没有涉及初始路由的概念。这是因为被缓存的引擎理论上已经执行了 Dart 代码,在这时配置初始路由已经太迟了。

The concept of an initial route is available when configuring a FlutterActivity or a FlutterFragment with a new FlutterEngine. However, FlutterActivity and FlutterFragment don’t offer the concept of an initial route when using a cached engine. This is because a cached engine is expected to already be running Dart code, which means it’s too late to configure the initial route.

开发者如果想要让缓存中的引擎从自定义的初始路由开始运行,那么可以执行 Dart 入口前,为缓存的 FlutterEngine 配置自定义的初始路由。如下面这个例子:

Developers that would like their cached engine to begin with a custom initial route can configure their cached FlutterEngine to use a custom initial route just before executing the Dart entrypoint. The following example demonstrates the use of an initial route with a cached engine:

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // Instantiate a FlutterEngine.
    flutterEngine = new FlutterEngine(this);
    // Configure an initial route.
    flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}
class MyApplication : Application() {
  lateinit var flutterEngine : FlutterEngine
  override fun onCreate() {
    super.onCreate()
    // Instantiate a FlutterEngine.
    flutterEngine = FlutterEngine(this)
    // Configure an initial route.
    flutterEngine.navigationChannel.setInitialRoute("your/route/here");
    // Start executing Dart code to pre-warm the FlutterEngine.
    flutterEngine.dartExecutor.executeDartEntrypoint(
      DartExecutor.DartEntrypoint.createDefault()
    )
    // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine)
  }
}

通过设置导航通道中的初始路由,会让关联的 FlutterEnginerunApp() 方法首次执行后,展示已配置的路由页面。

By setting the initial route of the navigation channel, the associated FlutterEngine displays the desired route upon initial execution of the runApp() Dart function.

runApp() 的首次执行之后,修改导航通道中的初始路由属性是不会生效的。想要在不同的 ActivityFragment 之间使用同一个 FlutterEngine,并且在其展示时切换不同的路由,开发者需要设置一个方法通道,来显式地通知他们的 Dart 代码切换 Navigator 路由。

Changing the initial route property of the navigation channel after the initial execution of runApp() has no effect. Developers who would like to use the same FlutterEngine between different Activitys and Fragments and switch the route between those displays need to setup a method channel and explicitly instruct their Dart code to change Navigator routes.

展示闪屏页

Display a splash screen

即使使用了预热的 FlutterEngine,第一次展示 Flutter 的内容仍然需要一些时间。为了更进一步提升用户体验,Flutter 支持在第一帧渲染完成之前展示闪屏页。关于如何展示闪屏页的详细说明,请参阅这篇 闪屏页指南

The initial display of Flutter content requires some wait time, even if a pre-warmed FlutterEngine is used. To help improve the user experience around this brief waiting period, Flutter supports the display of a splash screen until Flutter renders its first frame. For instructions about how to show a splash screen, see the splash screen guide.

指定 Flutter 运行的初始路由

Run Flutter with a specified initial route

一个 Android 应用中可能包含很多独立的 Flutter 界面,这些界面显示在不同的 FlutterFragment 上,每个 FlutterFragmentFlutterEngine 也是独立的。在这些情况下,每个 Flutter 界面通过不同的初始路由(除 / 以外的路由)启动是很正常的。为此,FlutterFragmentBuilder 允许指定一个您希望的初始路由,如下所示:

An Android app might contain many independent Flutter experiences, running in different FlutterFragments, with different FlutterEngines. In these scenarios, it’s common for each Flutter experience to begin with different initial routes (routes other than /). To facilitate this, FlutterFragment’s Builder allows you to specify a desired initial route, as shown:

// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build();
// With a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .initialRoute("myInitialRoute/")
    .build()

指定 Flutter 运行的入口

Run Flutter from a specified entrypoint

和变化的初始路由类似,不同的 FlutterFragment 可能需要执行不同的 Dart 代码入口。正常的 Flutter 应用中,只会有一个 main() 入口,但是您也可以定义不同的入口。

Similar to varying initial routes, different FlutterFragments may want to execute different Dart entrypoints. In a typical Flutter app, there is only one Dart entrypoint: main(), but you can define other entrypoints.

FlutterFragment 支持指定需要的 Dart 入口以运行对应的 Flutter 界面。下面的代码展示了如何在构建 FlutterFragment 时指定一个入口。

FlutterFragment supports specification of the desired Dart entrypoint to execute for the given Flutter experience. To specify an entrypoint, build FlutterFragment, as shown:

FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build();
val flutterFragment = FlutterFragment.withNewEngine()
    .dartEntrypoint("mySpecialEntrypoint")
    .build()

这里,FlutterFragment 的配置会将 Dart 入口的执行函数设置为 mySpecialEntrypoint()。需要注意的是,括号 () 不包含在 dartEntrypointString 类型的参数中。

The FlutterFragment configuration results in the execution of a Dart entrypoint called mySpecialEntrypoint(). Notice that the parentheses () are not included in the dartEntrypoint String name.

控制 FlutterFragment 的渲染模式

Control FlutterFragment’s render mode

FlutterFragment 可以选择使用 SurfaceView 或者 TextureView 来渲染其内容。默认配置的 SurfaceView 在性能上明显好于 TextureView。然而,SurfaceView 无法插入到 Android 的 View 层级之中。 SurfaceView 在视图层级中必须是最底层的 View 或者最顶层的 View。此外,在 Android N 之前,SurfaceView 无法用于制作动画,因为它们的布局和渲染无法和视图层级中的其它 View 同步。如果上述这些用例之一在您的应用需求之中,您需要使用 TextureView 替换 SurfaceView。要选择 TextureView,可以在构建 FlutterFragment 时指定 RenderModetexture

FlutterFragment can either use a SurfaceView to render its Flutter content, or it can use a TextureView. The default is SurfaceView, which is significantly better for performance than TextureView. However, SurfaceView can’t be interleaved in the middle of an Android View hierarchy. A SurfaceView must either be the bottommost View in the hierarchy, or the topmost View in the hierarchy. Additionally, on Android versions before Android N, SurfaceViews can’t be animated because their layout and rendering aren’t synchronized with the rest of the View hierarchy. If either of these use cases are requirements for your app, then you need to use TextureView instead of SurfaceView. Select a TextureView by building a FlutterFragment with a texture RenderMode:

// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build();

// With a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build();
// With a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build()

// With a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build()

使用上面展示的代码配置,FlutterFragment 可以将它的 UI 渲染为 TextureView

Using the configuration shown, the resulting FlutterFragment renders its UI to a TextureView.

展示透明的 FlutterFragment

Display a FlutterFragment with transparency

默认情况下,FlutterFragment 使用 SurfaceView 渲染且背景不透明。(参考「控制 FlutterFragment 的渲染模式」)任何未经 Flutter 绘制的像素在背景中都是黑色的。出于性能方面的考虑,我们优先选择使用不透明的背景进行渲染。渲染透明的 Flutter 界面在 Android 平台上会产生性能方面的负面影响。但是许多设计都需要 Flutter 界面中包含透明的像素以显示底层的 Android UI。因此,Flutter 支持 FlutterFragment 半透明。

By default, FlutterFragment renders with an opaque background, using a SurfaceView. (See “Control FlutterFragment’s render mode.”) That background is black for any pixels that aren’t painted by Flutter. Rendering with an opaque background is the preferred rendering mode for performance reasons. Flutter rendering with transparency on Android negatively affects performance. However, there are many designs that require transparent pixels in the Flutter experience that show through to the underlying Android UI. For this reason, Flutter supports translucency in a FlutterFragment.

要启动一个透明的 FlutterFragment,可以使用以下方式进行构建:

To enable transparency for a FlutterFragment, build it with the following configuration:

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();
// Using a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()

// Using a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build()

FlutterFragment 与其 Activity 之间的关系

The relationship between FlutterFragment and its Activity

一些应用选择使用 Fragment 作为整个 Android 屏幕内容。在这些应用里,Fragment 可能会需要控制一些系统属性,例如 Android 的状态栏、导航栏以及屏幕方向。

Some apps choose to use Fragments as entire Android screens. In these apps, it would be reasonable for a Fragment to control system chrome like Android’s status bar, navigation bar, and orientation.

Fullscreen Flutter

在其它应用中,Fragment 通常只是整个 UI 的一部分。 FlutterFragment 可能用于实现抽屉、视频播放器或卡片的内容。在这些情况下,FlutterFragment 就不应当影响 Android 的系统属性,因为同一个 Window 中还有其它 UI 组件。

In other apps, Fragments are used to represent only a portion of a UI. A FlutterFragment might be used to implement the inside of a drawer, a video player, or a single card. In these situations, it would be inappropriate for the FlutterFragment to affect Android’s system chrome because there are other UI pieces within the same Window.

Flutter as Partial UI

FlutterFragment 自身包含一种特性,可以用于决定 FlutterFragment 是否应该控制宿主 Activity,或者只影响自身行为。要预防 FlutterFragment 将其 Activity 暴露给 Flutter 插件,以免 Flutter 控制 Activity 的系统 UI,可以使用 FlutterFragmentBuilder 中的 shouldAttachEngineToActivity() 方法。如下所示:

FlutterFragment comes with a concept that helps differentiate between the case when a FlutterFragment should be able to control its host Activity, and the cases when a FlutterFragment should only affect its own behavior. To prevent a FlutterFragment from exposing its Activity to Flutter plugins, and to prevent Flutter from controlling the Activity’s system UI, use the shouldAttachEngineToActivity() method in FlutterFragment’s Builder, as shown:

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build();
// Using a new FlutterEngine.
val flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build()

// Using a cached FlutterEngine.
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build()

传递 falseBuildershouldAttachEngineToActivity() 方法,可防止 Flutter 与所属的 Activity 交互。默认值为 true,此时允许 Flutter 和 Flutter 插件与 Activity 交互。

Passing false to the shouldAttachEngineToActivity() Builder method prevents Flutter from interacting with the surrounding Activity. The default value is true, which allows Flutter and Flutter plugins to interact with the surrounding Activity.

Adding a Flutter View to an Android app

目录

Integrating via a FlutterView requires a bit more work than via FlutterActivity and FlutterFragment previously described.

Fundamentally, the Flutter framework on the Dart side requires access to various activity-level events and lifecycles to function. Since the FlutterView (which is an android.view.View) can be added to any activity which is owned by the developer’s application and since the FlutterView doesn’t have access to activity level events, the developer must bridge those connections manually to the FlutterEngine.

How you choose to feed your application’s activities’ events to the FlutterView will be specific to your application.

A sample

Add Flutter View sample video

Unlike the guides for FlutterActivity and FlutterFragment, the FlutterView integration could be better demonstrated with a sample project.

A sample project is at https://github.com/flutter/samples/tree/master/add_to_app/android_view to document a simple FlutterView integration where FlutterViews are used for some of the cells in a RecycleView list of cards as seen in the gif above.

General approach

The general gist of the FlutterView-level integration is that you must recreate the various interactions between the your Activity, the FlutterView and the FlutterEngine present in the FlutterActivityAndFragmentDelegate in your own application’s code. The connections made in the FlutterActivityAndFragmentDelegate are done automatically when using a FlutterActivity or a FlutterFragment, but since the FlutterView in this case is being added to an Activity or Fragment in your application, you must recreate the connections manually. Otherwise, the FlutterView will not render anything or have other missing functionalities.

A sample FlutterViewEngine class shows one such possible implementation of an application-specific connection between an Activity, a FlutterView and a FlutterEngine.

APIs to implement

The absolute minimum implementation needed for Flutter to draw anything at all is to:

The reverse detachFromFlutterEngine and other lifecycle methods on the LifecycleChannel class must also be called to not leak resources when the FlutterView or Activity is no longer visible.

In addition, see the remaining implementation in the FlutterViewEngine demo class or in the FlutterActivityAndFragmentDelegate to ensure a correct functioning of other features such as clipboards, system UI overlay, plugins etc.

在混合应用中管理 plugin 和依赖

目录

This guide describes how to set up your project to consume plugins and how to manage your Gradle library dependencies between your existing Android app and your Flutter module’s plugins.

A. Simple scenario

In the simple cases:

There are no additional steps needed. Your add-to-app module will work the same way as a full-Flutter app. Whether you integrate using Android Studio, Gradle subproject or AARs, transitive Android Gradle libraries are automatically bundled as needed into your outer existing app.

B. Plugins needing project edits

Some plugins require you to make some edits to the Android side of your project.

For example, the integration instructions for the firebase_crashlytics plugin require manual edits to your Android wrapper project’s build.gradle file.

For full-Flutter apps, these edits are done in your Flutter project’s /android/ directory.

In the case of a Flutter module, there are only Dart files in your module project. Perform those Android Gradle file edits on your outer, existing Android app rather than in your Flutter module.

C. Merging libraries

The scenario that requires slightly more attention is if your existing Android application already depends on the same Android library that your Flutter module does (transitively via a plugin).

For instance, your existing app’s Gradle may already have:


dependencies {
  
  implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
  
}

And your Flutter module also depends on firebase_crashlytics via pubspec.yaml:


dependencies:
  
  firebase_crashlytics: ^0.1.3
  

This plugin usage transitively adds a Gradle dependency again via firebase_crashlytics v0.1.3’s own Gradle file:


dependencies {
  
  implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
  
}

The two com.crashlytics.sdk.android:crashlytics dependencies might not be the same version. In this example, the host app requested v2.10.1 and the Flutter module plugin requested v2.9.9.

By default, Gradle v5 resolves dependency version conflicts by using the newest version of the library.

This is generally ok as long as there are no API or implementation breaking changes between the versions. For example, you might use the new Crashlytics library in your existing app as follows:


dependencies {
  
  implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta03
  
}

This approach won’t work since there are major API differences between the Crashlytics’ Gradle library version v17.0.0-beta03 and v2.9.9.

For Gradle libraries that follow semantic versioning, you can generally avoid compilation and runtime errors by using the same major semantic version in your existing app and Flutter module plugin.

将 Flutter module 集成到 iOS 项目

目录

Flutter 可以以 framework 框架的形式添加到你的既有 iOS 应用中。

Flutter can be incrementally added into your existing iOS application as embedded frameworks.

请参阅 add_to_app代码示例 的 iOS 目录。

For examples, see the iOS directories in the add_to_app code samples.

系统要求

System requirements

你的开发环境必须满足 Flutter 对 macOS 系统的版本要求已经安装 Xcode,Flutter 支持 iOS 8.0 及以上。此外,你还需要 1.10 或以上版本的 CocoaPods

Your development environment must meet the macOS system requirements for Flutter with Xcode installed. Flutter supports iOS 9.0 and later. Additionally, you will need CocoaPods version 1.10 or later.

创建 Flutter module

Create a Flutter module

为了将 Flutter 集成到你的既有应用里,第一步要创建一个 Flutter module。

To embed Flutter into your existing application, first create a Flutter module.

在命令行中执行:

From the command line, run:

cd some/path/
flutter create --template module my_flutter

Flutter module 会创建在 some/path/my_flutter/ 目录。在这个目录中,你可以像在其它 Flutter 项目中一样,执行 flutter 命令。比如 flutter run --debug 或者 flutter build ios。同样,你也可以通过 Android Studio/IntelliJ 或者 VS Code 中的 Flutter 和 Dart 插件运行这个模块,在集成到现有应用前,这个项目在 Flutter module 中包含了一个单视图的示例代码,对 Flutter 侧代码的测试会有帮助。

A Flutter module project is created at some/path/my_flutter/. From that directory, you can run the same flutter commands you would in any other Flutter project, like flutter run --debug or flutter build ios. You can also run the module in Android Studio/IntelliJ or VS Code with the Flutter and Dart plugins. This project contains a single-view example version of your module before it’s embedded in your existing application, which is useful for incrementally testing the Flutter-only parts of your code.

模块组织

Module organization

my_flutter 模块,目录结构和普通 Flutter 应用类似:

The my_flutter module directory structure is similar to a normal Flutter application:

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

添加你的 Dart 代码到 lib/ 目录。

Add your Dart code to the lib/ directory.

添加 Flutter 依赖到 my_flutter/pubspec.yaml,包括 Flutter packages 和 plugins。

Add Flutter dependencies to my_flutter/pubspec.yaml, including Flutter packages and plugins.

.ios/ 隐藏文件夹包含了一个 Xcode workspace,用于单独运行你的 Flutter module。它是一个独立启动 Flutter 代码的壳工程,并且包含了一个帮助脚本,用于编译 framewroks 或者使用 CocoaPods 将 Flutter module 集成到你的既有应用。

The .ios/ hidden subfolder contains an Xcode workspace where you can run a standalone version of your module. It is a wrapper project to bootstrap your Flutter code, and contains helper scripts to facilitate building frameworks or embedding the module into your existing application with CocoaPods.

在你的既有应用中集成 Flutter module

Embed the Flutter module in your existing application

这里有两种方式可以将 Flutter 集成到你的既有应用中。

There are two ways to embed Flutter in your existing application.

  1. 使用 CocoaPods 依赖管理和已安装的 Flutter SDK 。(推荐)

    Use the CocoaPods dependency manager and installed Flutter SDK. (Recommended.)

  2. 把 Flutter engine 、你的 dart 代码和所有 Flutter plugin 编译成 framework 。用 Xcode 手动集成到你的应用中,并更新编译设置。

    Create frameworks for the Flutter engine, your compiled Dart code, and all Flutter plugins. Manually embed the frameworks, and update your existing application’s build settings in Xcode.

使用 Flutter 会 增加应用体积

Using Flutter increases your app size.

选项 A - 使用 CocoaPods 和 Flutter SDK 集成

Option A - Embed with CocoaPods and the Flutter SDK

这个方法需要你的项目的所有开发者,都在本地安装 Flutter SDK。只需要在 Xcode 中编译应用,就可以自动运行脚本来集成 dart 代码和 plugin。这个方法允许你使用 Flutter module 中的最新代码快速迭代开发,而无需在 Xcode 以外执行额外的命令。

This method requires every developer working on your project to have a locally installed version of the Flutter SDK. Simply build your application in Xcode to automatically run the script to embed your Dart and plugin code. This allows rapid iteration with the most up-to-date version of your Flutter module without running additional commands outside of Xcode.

下面的示例假设你的既有应用和 Flutter module 在相邻目录。如果你有不同的目录结构,需要适配到对应的路径。

The following example assumes that your existing application and the Flutter module are in sibling directories. If you have a different directory structure, you may need to adjust the relative paths.

some/path/
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/
    └── Podfile

如果你的应用(MyApp)还没有 Podfile,根据 CocoaPods getting started guide 来在项目中添加 Podfile

If your existing application (MyApp) doesn’t already have a Podfile, follow the CocoaPods getting started guide to add a Podfile to your project.

  1. Podfile 中添加下面代码:

    Add the following lines to your Podfile:

    flutter_application_path = '../my_flutter'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    
  2. 每个需要集成 Flutter 的 Podfile target,执行 install_all_flutter_pods(flutter_application_path)

    For each Podfile target that needs to embed Flutter, call install_all_flutter_pods(flutter_application_path).

    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
    
  3. 运行 pod install

    Run pod install.

podhelper.rb 脚本会把你的 plugins, Flutter.framework,和 App.framework 集成到你的项目中。

The podhelper.rb script embeds your plugins, Flutter.framework, and App.framework into your project.

你应用的 Debug 和 Release 编译配置,将会集成相对应的 Debug 或 Release 的 编译产物。可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。

Your app’s Debug and Release build configurations embed the Debug or Release build modes of Flutter, respectively. Add a Profile build configuration to your app to test in profile mode.

在 Xcode 中打开 MyApp.xcworkspace ,你现在可以使用 ⌘B 编译项目了。

Open MyApp.xcworkspace in Xcode. You can now build the project using ⌘B.

选项 B - 在 Xcode 中集成 frameworks

Option B - Embed frameworks in Xcode

除了上面的方法,你也可以创建必备的 frameworks,手动修改既有 Xcode 项目,将他们集成进去。当你组内其它成员们不能在本地安装 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作为既有应用的依赖管理时,这种方法会比较合适。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework

Alternatively, you can generate the necessary frameworks and embed them in your application by manually editing your existing Xcode project. You may do this if members of your team can’t locally install Flutter SDK and CocoaPods, or if you don’t want to use CocoaPods as a dependency manager in your existing applications. You must run flutter build ios-framework every time you make code changes in your Flutter module.

如果你使用前面的 使用 CocoaPods 和 Flutter SDK 集成,你可以跳过本步骤。

If you’re using the previous Embed with CocoaPods and Flutter tools method, you can skip these instructions.

下面的示例假设你想在 some/path/MyApp/Flutter/ 目录下创建 frameworks:

The following example assumes that you want to generate the frameworks to some/path/MyApp/Flutter/.

flutter build ios-framework --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code)
    │   └── example_plugin.xcframework (each plugin is a separate framework)
    ├── Profile/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.xcframework
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

在 Xcode 中将生成的 frameworks 集成到你的既有应用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。

Embed and link the generated frameworks into your existing application in Xcode. There are multiple ways to do this—use the method that is best for your project.

Link on the frameworks

例如,你可以将框架从 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目标项目中,然后点击以下步骤 build settings > Build Phases > Link Binary With Libraries。

For example, you can drag the frameworks from some/path/MyApp/Flutter/Release/ in Finder into your target’s Build Settings > Build Phases > Link Binary With Libraries.

在 target 的编译设置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/

In the target’s build settings, add $(PROJECT_DIR)/Flutter/Release/ to the Framework Search Paths (FRAMEWORK_SEARCH_PATHS).

Update Framework Search Paths in Xcode

Embed the frameworks

内嵌框架

生成的动态框架必须嵌入你的应用并且在运行时被加载。

The generated dynamic frameworks must be embedded into your app to be loaded at runtime.

例如,你可以从应用框架组中拖拽框架(除了 FlutterPluginRegistrant 以及其他的静态框架)到你的目标 ‘ build settings > Build Phases > Embed Frameworks。然后从下拉菜单中选择 “Embed & Sign”。

For example, you can drag the framework (except for FlutterPluginRegistrant and any other static frameworks) from your application’s Frameworks group into your target’s Build Settings > Build Phases > Embed Frameworks. Then, select Embed & Sign from the drop-down list.

Embed frameworks in Xcode

你现在可以在 Xcode中使用 ⌘B 编译项目。

You should now be able to build the project in Xcode using ⌘B.

选项 C - 使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用和插件框架

Option C - Embed application and plugin frameworks in Xcode and Flutter framework with CocoaPods

除了将一个很大的 Flutter.framework 分发给其他开发者、机器或者持续集成 (CI) 系统之外,你可以加入一个参数 --cocoapods 将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件。如选项 B 中所说的那样,它将会生成 App.framework 和插件框架。

Alternatively, instead of distributing the large Flutter.xcframework to other developers, machines, or continuous integration systems, you can instead generate Flutter as CocoaPods podspec by adding the flag --cocoapods. This produces a Flutter.podspec instead of an engine Flutter.xcframework. The App.xcframework and plugin frameworks are generated as described in Option B.

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework (each plugin with iOS platform code is a separate framework)
    ├── Profile/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.podspec
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

Host apps using CocoaPods can add Flutter to their Podfile:

pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'

Embed and link the generated App.xcframework, FlutterPluginRegistrant.xcframework, and any plugin frameworks into your existing application as described in Option B.

Local Network Privacy Permissions

On iOS 14 and higher, enable the Dart multicast DNS service in the Debug version of your app to add debugging functionalities such as hot-reload and DevTools via flutter attach.

One way to do this is to maintain a separate copy of your app’s Info.plist per build configuration. The following instructions assume the default Debug and Release. Adjust the names as needed depending on your app’s build configurations.

  1. Rename your app’s Info.plist to Info-Debug.plist. Make a copy of it called Info-Release.plist and add it to your Xcode project.

    Info-Debug.plist and Info-Release.plist in Xcode
  2. In Info-Debug.plist only add the key NSBonjourServices and set the value to an array with the string _dartobservatory._tcp. Note Xcode will display this as “Bonjour services”.

    Optionally, add the key NSLocalNetworkUsageDescription set to your desired customized permission dialog text.

    Info-Debug.plist with additional keys
  3. In your target’s build settings, change the Info.plist File (INFOPLIST_FILE) setting path from path/to/Info.plist to path/to/Info-$(CONFIGURATION).plist.

    Set INFOPLIST_FILE build setting

    This will resolve to the path Info-Debug.plist in Debug and Info-Release.plist in Release.

    Resolved INFOPLIST_FILE build setting

    Alternatively, you can explicitly set the Debug path to Info-Debug.plist and the Release path to Info-Release.plist.

  4. If the Info-Release.plist copy is in your target’s Build Settings > Build Phases > Copy Bundle Resources build phase, remove it.

    Copy Bundle build phase

    The first Flutter screen loaded by your Debug app will now prompt for local network permission. The permission can also be allowed by enabling Settings > Privacy > Local Network > Your App.

    Local network permission dialog

Apple Silicon (arm64 Macs)

Flutter 目前暂未支持 arm64 的 iOS 模拟器。要在 Apple Silicon Mac 设备上运行你的宿主应用,请从模拟器支持架构中移除 arm64

Flutter does not yet support arm64 iOS simulators. To run your host app on an Apple Silicon Mac, exclude arm64 from the simulator architectures.

在宿主应用的 Target 中,找到名为 Excluded Architectures (EXCLUDED_ARCHS) 的构建设置。单击右侧的箭头指示器图标以展开可用的构建配置。将鼠标悬停在 Debug 处并单击加号图标。将 Any SDK 更改为 Any iOS Simulator SDK。然后向构建设置值中添加 arm64

In your host app target, find the Excluded Architectures (EXCLUDED_ARCHS) build setting. Click the right arrow disclosure indicator icon to expand the available build configurations. Hover over Debug and click the plus icon. Change Any SDK to Any iOS Simulator SDK. Add arm64 to the build settings value.

Set conditional EXCLUDED_ARCHS build setting

当全部都正确设置后,Xcode 将会向你的 project.pbxproj 文件中添加 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;

When done correctly, Xcode will add "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; to your project.pbxproj file.

然后对全部 iOS 目标再次执行单元测试。

Repeat for any iOS unit test targets.

开发

Development

你现在可以 添加一个 Flutter 页面 到你的既有应用中。

You can now add a Flutter screen to your existing application.

在 iOS 应用中添加 Flutter 页面

目录

本指南描述了怎样在既有 iOS 应用中添加单个 Flutter 页面。

This guide describes how to add a single Flutter screen to an existing iOS app.

启动 FlutterEngine 和 FlutterViewController

Start a FlutterEngine and FlutterViewController

为了在既有 iOS 应用中展示 Flutter 页面,请启动 FlutterEngineFlutterViewController

To launch a Flutter screen from an existing iOS, you start a FlutterEngine and a FlutterViewController.

FlutterEngine 的寿命可能与 FlutterViewController 相同,也可能超过 FlutterViewController

The FlutterEngine may have the same lifespan as your FlutterViewController or outlive your FlutterViewController.

加载顺序和性能 里有更多关于预热 engine 的延迟和内存取舍的分析。

See Loading sequence and performance for more analysis on the latency and memory trade-offs of pre-warming an engine.

创建一个 FlutterEngine

Create a FlutterEngine

创建 FlutterEngine 的合适位置取决于您的应用。作为示例,我们将在应用启动的 app delegate 中创建一个 FlutterEngine,并作为属性暴露给外界。

The proper place to create a FlutterEngine is specific to your host app. As an example, we demonstrate creating a FlutterEngine, exposed as a property, on app startup in the app delegate.

AppDelegate.h:

In AppDelegate.h:

@import UIKit;
@import Flutter;

@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

AppDelegate.m:

In AppDelegate.m:

// Used to connect plugins (only if you have plugins with iOS platform code).
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

#import "AppDelegate.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
  // Runs the default Dart entrypoint with a default Flutter route.
  [self.flutterEngine run];
  // Used to connect plugins (only if you have plugins with iOS platform code).
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

AppDelegate.swift:

In AppDelegate.swift:

import UIKit
import Flutter
// Used to connect plugins (only if you have plugins with iOS platform code).
import FlutterPluginRegistrant

@UIApplicationMain
class AppDelegate: FlutterAppDelegate { // More on the FlutterAppDelegate.
  lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Runs the default Dart entrypoint with a default Flutter route.
    flutterEngine.run();
    // Used to connect plugins (only if you have plugins with iOS platform code).
    GeneratedPluginRegistrant.register(with: self.flutterEngine);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }
}

使用 FlutterEngine 展示 FlutterViewController

Show a FlutterViewController with your FlutterEngine

下面的例子展示了一个普通的 ViewController,包含一个能跳转到 FlutterViewControllerUIButton,这个 FlutterViewController 使用在 AppDelegate 中创建的 Flutter 引擎 (FlutterEngine)。

The following example shows a generic ViewController with a UIButton hooked to present a FlutterViewController. The FlutterViewController uses the FlutterEngine instance created in the AppDelegate.

@import Flutter;
#import "AppDelegate.h"
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // Make a button to call the showFlutter function when pressed.
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(showFlutter)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
    button.backgroundColor = UIColor.blueColor;
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)showFlutter {
    FlutterEngine *flutterEngine =
        ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
    FlutterViewController *flutterViewController =
        [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
import UIKit
import Flutter

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Make a button to call the showFlutter function when pressed.
    let button = UIButton(type:UIButton.ButtonType.custom)
    button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
    button.setTitle("Show Flutter!", for: UIControl.State.normal)
    button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
    button.backgroundColor = UIColor.blue
    self.view.addSubview(button)
  }

  @objc func showFlutter() {
    let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
    let flutterViewController =
        FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    present(flutterViewController, animated: true, completion: nil)
  }
}

现在,你的 iOS 应用中集成了一个 Flutter 页面。

Now, you have a Flutter screen embedded in your iOS app.

或者 —— 使用隐式 FlutterEngine 创建 FlutterViewController

Alternatively - Create a FlutterViewController with an implicit FlutterEngine

上一个示例还有另一个选择,你可以让 FlutterViewController 隐式创建它自己的 FlutterEngine,而不用提前预热 engine。

As an alternative to the previous example, you can let the FlutterViewController implicitly create its own FlutterEngine without pre-warming one ahead of time.

不过不建议这样做,因为按需创建FlutterEngine 的话,在 FlutterViewController 被 present 出来之后,第一帧图像渲染完之前,将会引入明显的延迟。但是当 Flutter 页面很少被展示时,当对决定何时启动 Dart VM 没有好的启发时,当 Flutter 无需在页面(view controller)之间保持状态时,此方式可能会有用。

This is not usually recommended because creating a FlutterEngine on-demand could introduce a noticeable latency between when the FlutterViewController is presented and when it renders its first frame. This could, however, be useful if the Flutter screen is rarely shown, when there are no good heuristics to determine when the Dart VM should be started, and when Flutter doesn’t need to persist state between view controllers.

为了不使用已经存在的 FlutterEngine 来展现 FlutterViewController,省略 FlutterEngine 的创建步骤,并且在创建 FlutterViewController 时,去掉 engine 的引用。

To let the FlutterViewController present without an existing FlutterEngine, omit the FlutterEngine construction, and create the FlutterViewController without an engine reference.

// Existing code omitted.
// 省略已经存在的代码
- (void)showFlutter {
  FlutterViewController *flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
  [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
// Existing code omitted.
// 省略已经存在的代码
func showFlutter() {
  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}

查看 加载顺序和性能 了解更多关于延迟和内存使用的探索。

See Loading sequence and performance for more explorations on latency and memory usage.

使用 FlutterAppDelegate

Using the FlutterAppDelegate

推荐让你应用的 UIApplicationDelegate 继承 FlutterAppDelegate,但不是必须的。

Letting your application’s UIApplicationDelegate subclass FlutterAppDelegate is recommended but not required.

FlutterAppDelegate 有这些功能:

The FlutterAppDelegate performs functions such as:

如果你的 app delegate 不能直接继承 FlutterAppDelegate,让你的 app delegate 实现 FlutterAppLifeCycleProvider 协议,来确保 Flutter plugins 接收到必要的回调。否则,依赖这些事件的 plugins 将会有无法预估的行为。

If your app delegate can’t directly make FlutterAppDelegate a subclass, make your app delegate implement the FlutterAppLifeCycleProvider protocol in order to make sure your plugins receive the necessary callbacks. Otherwise, plugins that depend on these events may have undefined behavior.

例如:

For instance:

@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

在具体实现中,应该最大化地委托给 FlutterPluginAppLifeCycleDelegate

The implementation should delegate mostly to a FlutterPluginAppLifeCycleDelegate:

@interface AppDelegate ()
@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;
@end

@implementation AppDelegate

- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
    [self.flutterEngine runWithEntrypoint:nil];
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];

    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

启动选项

Launch options

例子中展示了使用默认启动选项运行 Flutter。

The examples demonstrate running Flutter using the default launch settings.

为了定制化你的 Flutter 运行时,你也可以置顶 Dart 入口、库和路由。

In order to customize your Flutter runtime, you can also specify the Dart entrypoint, library, and route.

Dart 入口

Dart entrypoint

FlutterEngine 上调用 run,默认将会调用你的 lib/main.dart 文件里的 main() 函数。

Calling run on a FlutterEngine, by default, runs the main() Dart function of your lib/main.dart file.

你也可以使用另一个入口方法 runWithEntrypoint,并使用 NSString 字符串指定一个不同的 Dart 入口。

You can also run a different entrypoint function by using runWithEntrypoint with an NSString specifying a different Dart function.

Dart 库

Dart library

另外,在指定 Dart 函数时,你可以指定特定文件的特定函数。

In addition to specifying a Dart function, you can specify an entrypoint function in a specific file.

下面的例子使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函数取代 lib/main.dartmain() 函数:

For instance the following runs myOtherEntrypoint() in lib/other_file.dart instead of main() in lib/main.dart:

[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")

路由

Route

当构建 engine 时,可以为你的 Flutter WidgetsApp 设置一个初始路由。

An initial route can be set for your Flutter WidgetsApp when constructing the engine.

FlutterEngine *flutterEngine =
    [[FlutterEngine alloc] initWithName:@"my flutter engine"];
[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"
                                      arguments:@"/onboarding"];
[flutterEngine run];
let flutterEngine = FlutterEngine(name: "my flutter engine")
flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding")
flutterEngine.run()

这段代码使用 "/onboarding" 取代 "/",作为你的 dart:uiwindow.defaultRouteName

This code sets your dart:ui’s window.defaultRouteName to "/onboarding" instead of "/".

FlutterViewController* flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil
                                        initialRoute:@"/onboarding"
                                             nibName:nil
                                              bundle:nil];
let flutterViewController = FlutterViewController(
      project: nil, initialRoute: "/onboarding", nibName: nil, bundle: nil)

查看文档:路由和导航 了解更多 Flutter 路由的内容。

See Navigation and routing for more about Flutter’s routes.

其它

Other

之前的例子仅仅展示了怎样定制 Flutter 实例初始化的几种方式,通过 撰写双端平台代码,你可以在 FlutterViewController 展示 Flutter UI 之前,自由地选择你喜欢的,推入数据和准备 Flutter 环境的方式。 The previous example only illustrates a few ways to customize how a Flutter instance is initiated. Using platform channels, you’re free to push data or prepare your Flutter environment in any way you’d like, before presenting the Flutter UI using a FlutterViewController.

The previous example only illustrates a few ways to customize how a Flutter instance is initiated. Using platform channels, you’re free to push data or prepare your Flutter environment in any way you’d like, before presenting the Flutter UI using a FlutterViewController.

在混合开发模式下进行调试

目录

调试混合开发的模块

Debugging your add-to-app module

当你将 Flutter 模块集成到项目中并使用 Flutter 的平台 APIs 来运行 Flutter 引擎和/或 UI 时,你可以与平时运行 Android 或 iOS 应用程序一样,构建和运行你的应用。

Once you’ve integrated the Flutter module to your project and used Flutter’s platform APIs to run the Flutter engine and/or UI, you can then build and run your Android or iOS app the same way you run normal Android or iOS apps.

但就目前而言,Flutter 需要在 FlutterActivityFlutterViewController 中展示 UI 内容。

However, Flutter is now powering the UI in places where you’re showing a FlutterActivity or FlutterViewController.

调试

Debugging

你可能习惯于在 IDE 中运行 flutter run 或者等效的快捷命令,它会自动启动你喜爱的 Flutter 调试工具。同样的,你也可以使用所有 Flutter 的 调试功能,例如热重载、性能调试、DevTools 以及在混合开发的场景中设置断点。

You may be used to having your suite of favorite Flutter debugging tools available to you automatically when running flutter run or an equivalent command from an IDE. But you can also use all your Flutter debugging functionalities such as hot reload, performance overlays, DevTools, and setting breakpoints in add-to-app scenarios.

这些功能由 flutter attach 机制提供。 flutter attach 可以通过不同的路径启动,例如通过 SDK 中的命令行工具、VS Code 或者 IntelliJ/Android Studio。

These functionalities are provided by the flutter attach mechanism. flutter attach can be initiated through different pathways, such as through the SDK’s CLI tools, through VS Code or IntelliJ/Android Studio.

flutter attach 可以在你运行 FlutterEngine 时立即进行连接,并在 FlutterEngine 被释放之前一直保持连接。你可以在启动引擎之前执行 flutter attach,它将等待下一个由引擎持有的 Dart VM 进行连接。

flutter attach can connect as soon as you run your FlutterEngine, and remains attached until your FlutterEngine is disposed. But you can invoke flutter attach before starting your engine. flutter attach waits for the next available Dart VM that is hosted by your engine.

终端

Terminal

在终端执行 flutter attach 或者 flutter attach -d deviceId 来连接你的应用。

Run flutter attach or flutter attach -d deviceId to attach from the terminal.

flutter attach via terminal
flutter attach via terminal

VS Code

在 VS Code 中的状态栏中选择待调试的设备,然后在命令面板运行 Flutter: Attach to Flutter on Device 命令。

Select the correct device using the status bar in VS Code, then run the Flutter: Attach to Flutter on Device command from the command palette.

flutter attach via VS Code
flutter attach via VS Code

或者,在你的 Flutter 模块下创建一个 .vscode/launch.json 文件,使用 Run > Start Debugging 命令或按下 F5

Alternatively, create a .vscode/launch.json file in your Flutter module project to enable attaching using the Run > Start Debugging command or F5:

{
  name: "Flutter: Attach",
  request: "attach",
  type: "dart",
}

IntelliJ / Android Studio

选择要运行 Flutter 模块的设备,然后点击右边的 flutter attach 按钮。

Select the device on which the Flutter module runs so flutter attach filters for the right start signals.

flutter attach via IntelliJ
flutter attach via IntelliJ

控制加载顺序,优化性能与内存

目录

本页面描述了展示一个 Flutter UI 的分解步骤。知道了这一点,您可以就何时对 Flutter 引擎进行预热,在哪个阶段可以进行哪些操作,以及这些操作的潜在问题和内存成本做出更好、更明智的决策。

This page describes the breakdown of the steps involved to show a Flutter UI. Knowing this, you can make better, more informed decisions about when to pre-warm the Flutter engine, which operations are possible at which stage, and the latency and memory costs of those operations.

加载 Flutter

Loading Flutter

在展示 Flutter UI 时, Android 与 iOS 应用(用于集成到现有应用的两个受支持的平台),纯 Flutter 应用,以及 add-to-app 的模式,在概念上的加载步骤顺序相似。

Android and iOS apps (the two supported platforms for integrating into existing apps), full Flutter apps, and add-to-app patterns have a similar sequence of conceptual loading steps when displaying the Flutter UI.

查找 Flutter 资源

Finding the Flutter resources

Flutter 的引擎运行时和应用已编译的 Dart 代码都被打包为 Android 和 iOS 上的共享库。加载 Flutter 的第一步是在 .apk、.ipa 或 .app 中查找这些资源(以及其他 Flutter 资源,例如图像和字体,假如适用的话还有 JIT 代码)。

Flutter’s engine runtime and your application’s compiled Dart code are both bundled as shared libraries on Android and iOS. The first step of loading Flutter is to find those resources in your .apk/.ipa/.app (along with other Flutter assets such as images, fonts, and JIT code, if applicable).

当您首次在 AndroidiOS 上调用 API 构建 FlutterEngine 时,就会发生这种情况。

This happens when you construct a FlutterEngine for the first time on both Android and iOS APIs.

加载 Flutter 库

Loading the Flutter library

找到后,引擎的共享库将在每个进程中加载一次内存。

After it’s found, the engine’s shared libraries are memory loaded once per process.

Android 上,当构建 FlutterEngine 时也会发生这种情况,因为 JNI 连接器需要引用 Flutter C++ 库。在 iOS 上,这是在首次运行 FlutterEngine 时发生的,例如运行 runWithEntrypoint:

On Android, this also happens when the FlutterEngine is constructed because the JNI connectors need to reference the Flutter C++ library. On iOS, this happens when the FlutterEngine is first run, such as by running runWithEntrypoint:.

启动 Dart VM

Starting the Dart VM

Dart 运行时负责管理 Dart 代码的 Dart 内存与异步。在 JIT 模式下,它还负责在运行时将 Dart 源代码编译为机器码。

The Dart runtime is responsible for managing Dart memory and concurrency for your Dart code. In JIT mode, it’s additionally responsible for compiling the Dart source code into machine code during runtime.

在 Android 和 iOS 上,每个应用程序会话都存在一个 Dart 运行时。

A single Dart runtime exists per application session on Android and iOS.

Android 上首次构建 FlutterEngine,以及在 iOS 上首次 运行 Dart 入口 时,将完成一次 Dart VM 启动。

A one-time Dart VM start is done when constructing the FlutterEngine for the first time on Android and when running a Dart entrypoint for the first time on iOS.

此时,您的 Dart 代码的 snapshot 也将从应用程序的文件加载到内存中。

At this point, your Dart code’s snapshot is also loaded into memory from your application’s files.

即使您直接使用 Dart SDK而没 Flutter 引擎,也会这样执行,这是一个通用的过程。

This is a generic process that also occurs if you used the Dart SDK directly, without the Flutter engine.

Dart VM 启动后永远不会关闭。

The Dart VM never shuts down after it’s started.

创建并运行一个 Dart Isolate

Creating and running a Dart Isolate

在初始化 Dart 运行时之后,下一步就是 Flutter 引擎对 Dart 运行时的使用。

After the Dart runtime is initialized, the Flutter engine’s usage of the Dart runtime is the next step.

这是通过在 Dart 运行时中启动 Dart Isolate 来完成的。 isolate 是 Dart 的内存和线程容器。此时在宿主平台上还创建了许多 辅助线程 来支持 isolate,例如用于解除 GPU 处理的线程和用于图像解码的线程。

This is done by starting a Dart Isolate in the Dart runtime. The isolate is Dart’s container for memory and threads. A number of auxiliary threads on the host platform are also created at this point to support the isolate, such as a thread for offloading GPU handling and another for image decoding.

每个 FlutterEngine 实例都存在一个 isolate,并且同一个 Dart VM 可以承载多个 isolate。

One isolate exists per FlutterEngine instance, and multiple isolates can be hosted by the same Dart VM.

Android 上,当您在 FlutterEngine 实例上调用 DartExecutor.executeDartEntrypoint() 时,就会发生这种情况。

On Android, this happens when you call DartExecutor.executeDartEntrypoint() on a FlutterEngine instance.

iOS 上,当您对 FlutterEngine 实例调用 runWithEntrypoint:时会发生这种情况。

On iOS, this happens when you call runWithEntrypoint: on a FlutterEngine.

此时,Dart 代码会执行默认的入口点方法 (默认是 main.dart 文件的 main() 方法) ,如果你在 main() 方法中调用 Flutter 的 runApp() 方法,则你的 Flutter 应用或库的 widget 树将会创建并构建。如果你需要阻止某些功能在 Flutter 代码中执行,则需要使用枚举值 AppLifecycleState.detached 表示其不绑定在任何 UI 组件上。

At this point, your Dart code’s selected entrypoint (the main() function of your Dart library’s main.dart file, by default) is executed. If you called the Flutter function runApp() in your main() function, then your Flutter app or your library’s widget tree is also created and built. If you need to prevent certain functionalities from executing in your Flutter code, then the AppLifecycleState.detached enum value indicates that the FlutterEngine isn’t attached to any UI components such as a FlutterViewController on iOS or a FlutterActivity on Android.

将 UI 挂载到 Flutter 引擎

Attaching a UI to the Flutter engine

启动后不久,一个标准的完整的 Flutter 应用程序便会达到此状态。

A standard, full Flutter app moves to reach this state as soon as the app is launched.

在 add-to-app 的场景中,例如通过在 Android 上使用 FlutterActivity.withCachedEngine() 方法构建的 Intent,调用 startActivity() 时,或者,在 iOS 上调用 initWithEngine: nibName: bundle:,展示实例化的 FlutterViewController,都会将 FlutterEngine 挂载到 UI 组件。

In an add-to-app scenario, this happens when you attach a FlutterEngine to a UI component such as by calling startActivity() with an Intent built using FlutterActivity.withCachedEngine() on Android. Or, by presenting a FlutterViewController initialized by using initWithEngine: nibName: bundle: on iOS.

如果在没有启动 Flutter UI 组件的情况下也是如此, 例如在 Android 上使用 FlutterActivity.createDefaultIntent() 或在 iOS 上使用 FlutterViewController initWithProject: nibName: bundle: 预热一个 FlutterEngine。在这些情况下,将创建一个隐式的 FlutterEngine

This is also the case if a Flutter UI component was launched without pre-warming a FlutterEngine such as with FlutterActivity.createDefaultIntent() on Android, or with FlutterViewController initWithProject: nibName: bundle: on iOS. An implicit FlutterEngine is created in these cases.

在后台,这两个平台的UI组件都为 FlutterEngine 提供了渲染层,例如 Android 上的 SurfaceiOS 上的 CAEAGLLayerCAMetalLayer

Behind the scene, both platform’s UI components provide the FlutterEngine with a rendering surface such as a Surface on Android or a CAEAGLLayer or CAMetalLayer on iOS.

此时,您的 Flutter 程序生成的 Layer 树将转换为 OpenGL(或 Vulkan 或 Metal)GPU 指令。

At this point, the Layer tree generated by your Flutter program, per frame, is converted into OpenGL (or Vulkan or Metal) GPU instructions.

内存和延迟

Memory and latency

显示 Flutter UI 会耗费不少时间。提前启动 Flutter 引擎可以降低时间开销。

Showing a Flutter UI has a non-trivial latency cost. This cost can be lessened by starting the Flutter engine ahead of time.

对于 add-to-app 的场景,预热相应的选择是,让您决定什么时候预加载 FlutterEngine (即加载 Flutter 库,启动 Dart VM 并在 isolate 中运行入口点),以及确定内存的占用与时间开销。您还需要知道,在将 UI 组件随后挂载到该 FlutterEngine 时,预热会如何影响 Flutter 渲染首帧的内存和时间开销。

The most relevant choice for add-to-app scenarios is for you to decide when to pre-load a FlutterEngine (that is, to load the Flutter library, start the Dart VM, and run entrypoint in an isolate), and what the memory and latency cost is of that pre-warm. You also need to know how the pre-warm affects the memory and latency cost of rendering a first Flutter frame when the UI component is subsequently attached to that FlutterEngine.

用 Flutter v1.10.3 版本,在 2015 年的低端设备上测试,release AOT 模式下,预热 FlutterEngine 的开销:

As of Flutter v1.10.3, and testing on a low-end 2015 class device in release-AOT mode, pre-warming the FlutterEngine costs:

Flutter 用户界面可以在预热期间被加载。所需时间与渲染出首帧的时间有关。

A Flutter UI can be attached during the pre-warm. The remaining time is joined to the time-to-first-frame latency.

在内存方面的开销(具体根据使用情况而定)可能是:

Memory-wise, a cost sample (variable, depending on the use case) could be:

在时间方面的开销(具体根据使用情况而定)可能是:

Latency-wise, a cost sample (variable, depending on the use case) could be:

应该对 FlutterEngine 进行预热,不应过于提早,以延迟内存占用,但又要避免 Flutter 引擎初始化的时机与显示 Flutter 的首帧的时机赶在一起。

The FlutterEngine should be pre-warmed late enough to delay the memory consumption needed but early enough to avoid combining the Flutter engine start-up time with the first frame latency of showing Flutter.

确切的时间取决于应用的结构与不断试探的结果。一个示例是在 Flutter 绘制屏幕之前将 Flutter 引擎加载到屏幕中。

The exact timing depends on the app’s structure and heuristics. An example would be to load the Flutter engine in the screen before the screen is drawn by Flutter.

引擎预热后,加载 UI 首帧的成本为:

Given an engine pre-warm, the first frame cost on UI attach is:

在内存方面,开销主要用于渲染的图形内存缓冲区,并且取决于屏幕大小。

Memory-wise, the cost is primarily the graphical memory buffer used for rendering and is dependent on the screen size.

在时间方面,开销主要是在等待系统回调,为 Flutter 提供渲染层,并编译其余无法预先预测的着色器程序。这是一次性的开销。

Latency-wise, the cost is primarily waiting for the OS callback to provide Flutter with a rendering surface and compiling the remaining shader programs that are not pre-emptively predictable. This is a one-time cost.

释放 Flutter UI 组件后,将释放与 UI 相关的内存。这不会影响 Flutter 状态(除非也释放了 FlutterEngine),状态位于 FlutterEngine 中。

When the Flutter UI component is released, the UI-related memory is freed. This doesn’t affect the Flutter state, which lives in the FlutterEngine (unless the FlutterEngine is also released).

关于创建多个 FlutterEngine 对性能影响的详细情况,请参考文档: 多个 Flutter 实例

For performance details on creating more than one FlutterEngine, see multiple Flutters.

多个 Flutter 页面或视图

目录

实验性

Experimental

目前在 Android 和 iOS 上,除了第一个 Flutter 实例以外,其他每一个实例的内存占用量大约为 180kB。

The current memory footprint for each additional Flutter instance beyond the first instance is ~180kB on Android and iOS.

随着 Flutter 2.0.0 正式版的发布,Flutter 实例之间将通过宿主平台的 平台通道(或 Pigeon)进行处理。若您对我们平台通信的里程碑感兴趣,或是有其他多个 Flutter 实例的问题,请查看 Issue 72009

As of the 2.0.0 release, communication between Flutter instances is handled using platform channels (or Pigeon) through the host platform. To see our roadmap on communication, or other multiple-Flutters issues, see Issue 72009.

使用场景

Scenarios

在 Flutter 2.0.0 发布之前,FlutterEngine 的多个实例和相关的 UI 可以同时启动,但是每个实例都有明显的延迟和固定的内存占用。

Before Flutter 2.0.0, multiple instances of FlutterEngine and its associated UI could be launched, but each instance came with significant latency and fixed memory cost.

多个 Flutter 实例在以下场景有优势:

Multiple Flutter instances can be useful in the following scenarios:

使用多个 Flutter 实例的优势在于,每一个实例互相独立,各自维护路由栈、UI 和应用状态。这简化了应用程序整体的状态保持考虑,并且进一步模块化。了解更多关于多个 Flutter 使用的动机和场景,请查看 flutter.cn/go/multiple-flutters

The advantage of using multiple Flutter instances is that each instance is independent and maintains its own internal navigation stack, UI, and application states. This simplifies the overall application code’s responsibility for state keeping and improves modularity. More details on the scenarios motivating the usage of multiple Flutters can be found at docs.flutter.dev/go/multiple-flutters.

Flutter 2.0.0 大幅减少了额外的 Flutter 引擎的内存占用,从 Android 上 约 19MB,iOS 上 约 13MB,降至 约 180kB。将固定成本减少了约 99% 后,您可以更自由地将多个 Flutter 集成至您的应用。

The 2.0.0 Flutter release drastically reduces the memory footprint of additional Flutter engines from ~19MB on Android and ~13MB on iOS, to ~180kB on Android and iOS. This ~99% fixed cost reduction allows the multiple Flutters pattern to be used more liberally in your add-to-app integration.

组件

Components

在 Android 和 iOS 上添加多个 Flutter 实例的主要 API 是基于新的 FlutterEngineGroup 类 (Android API, iOS API) 来创建 FlutterEngine 的,而不是通过以前的 FlutterEngine 构造。

The primary API for adding multiple Flutter instances on both Android and iOS is based on a new FlutterEngineGroup class (Android API, iOS API) to construct FlutterEngines, rather than the FlutterEngine constructors used previously.

尽管 FlutterEngine API 的用法简洁明了,但从 FlutterEngineGroup 生成的 FlutterEngine 具有常用共享资源(例如 GPU 上下文、字体度量和隔离线程的快照)的性能优势,从而加快首次渲染的速度、降低延迟并降低内存占用。

Whereas the FlutterEngine API was direct and easier to consume, the FlutterEngine spawned from the same FlutterEngineGroup have the performance advantage of sharing many of the common, reusable resources such as the GPU context, font metrics, and isolate group snapshot, leading to a faster initial rendering latency and lower memory footprint.

示例

Samples

您可以在 GitHub 仓库 上找到在 Android 和 iOS 上使用 FlutterEngineGroup 的示例。

You can find a sample demonstrating how to use FlutterEngineGroup on both Android and iOS on GitHub.

A sample demonstrating multiple-Flutters

在 Android Studio 或 IntelliJ 里开发 Flutter 应用

目录

安装和设置

Installation and setup

按照 编辑工具设定,安装 Dart 和 Flutter 插件。

Follow the Set up an editor instructions to install the Dart and Flutter plugins.

更新插件

Updating the plugins

插件的更新会定期发布,当有更新可用时,你会在 IDE 中收到提示。

Updates to the plugins are shipped on a regular basis. You should be prompted in the IDE when an update is available.

手动检查更新:

To check for updates manually:

  1. 打开设置(在 macOS 中点击 Android Studio > Check for Updates,在 Linux 中点击 Help > Check for Updates)。

    Open preferences (Android Studio > Check for Updates on macOS, Help > Check for Updates on Linux).

  2. 如果存在 dartflutter,更新他们。

    If dart or flutter are listed, update them.

创建项目

Creating projects

你可以通过多种方式来创建新项目。

You can create a new project in one of several ways.

创建新项目

Creating a new project

使用 Futter 应用模板创建新的 Flutter 项目:

To create a new Flutter project from the Flutter starter app template:

  1. 在 IDE 中,点击 Welcome 窗口,或者主窗口 File > New > Project 中的 New Project

    In the IDE, click New Project from the Welcome window or File > New > Project from the main IDE window.

  2. 在菜单中选择 Flutter SDK path,点击 Next

    Specify the Flutter SDK path and click Next.

  3. 输入你的 Project nameDescriptionProject location

    Enter your desired Project name, Description and Project location.

  4. 如果打算发布此应用,需要 设置公司域名

    If you might publish this app, set the company domain.

  5. 点击 Finish

    Click Finish.

从现有源码创建新项目

Creating a new project from existing source code

创建包含现有 Flutter 源码的新 Flutter 项目:

To create a new Flutter project containing existing Flutter source code files:

  1. 在 IDE 中,点击 Welcome 窗口,或者主窗口 File > New > Project 中的 Create New Project

    In the IDE, click Create New Project from the Welcome window or File > New > Project from the main IDE window.

  2. 在菜单中选择 Flutter,点击 Next

    Select Flutter in the menu, and click Next.

  3. Project location 下,输入或选择现有 Flutter 源码的文件目录。

    Under Project location enter, or browse to, the directory holding your existing Flutter source code files.

  4. 点击 Finish

    Click Finish.

编辑代码,和查看问题

Editing code and viewing issues

Dart 插件的代码分析,可以做到:

The Flutter plugin performs code analysis that enables the following:

运行和调试

Running and debugging

在主工具栏,可以运行和调试代码:

Running and debugging are controlled from the main toolbar:

Main IntelliJ toolbar

选择目标设备

Selecting a target

在 IDE 中打开 Flutter 项目时,你会在工具栏的右侧看到一组 Flutter 的特定按钮。

When a Flutter project is open in the IDE, you should see a set of Flutter-specific buttons on the right-hand side of the toolbar.

  1. 找到选择目标下拉按钮,点击它会显示出可用设备列表。

    Locate the Flutter Target Selector drop-down button. This shows a list of available targets.

  2. 选择你希望启动应用的设备。当连接设备或启动模拟器时,列表中将会加入新选项。

    Select the target you want your app to be started on. When you connect devices, or start simulators, additional entries appear.

不使用断点运行应用

Run app without breakpoints

  1. 点击工具栏中的 Play 按钮,或选择 Run > Run。底部的 Run 窗口会有日志输出:

    Click the Play icon in the toolbar, or invoke Run > Run. The bottom Run pane shows logs output.

使用断点运行应用

Run app with breakpoints

  1. 如果需要,在源代码中设置断点。

    If desired, set breakpoints in your source code.

  2. 点击工具栏中的 Debug 按钮,或选择 Run > Debug

    Click the Debug icon in the toolbar, or invoke Run > Debug.

    • 底部的 Debugger 窗口会显示出堆栈和变量信息。

      The bottom Debugger pane shows Stack Frames and Variables.

    • 底部的 Console 窗口会显示详细的日志输出。

      The bottom Console pane shows detailed logs output.

    • 调试基于默认的启动配置,如果需要自定义,点击选择目标下拉按钮,选择 Edit configuration 进行配置。

      Debugging is based on a default launch configuration. To customize this, click the drop-down button to the right of the device selector, and select Edit configuration.

快速编辑和查看效果

Fast edit and refresh development cycle

Flutter 有效加快开发周期。使用 热重载 功能,你可以在修改源码后,几乎马上看到效果。详细信息请查阅 使用热重载

Flutter offers a best-in-class developer cycle enabling you to see the effect of your changes almost instantly with the Stateful Hot Reload feature. See Hot reload for details.

显示性能数据

Show performance data

Debug 模式下启动应用后,使用 View > Tool Windows > Flutter Performance 打开性能工具窗口,以查看性能数据以及 widget 的 rebuild 信息。

To view the performance data, including the widget rebuild information, start the app in Debug mode, and then open the Performance tool window using View > Tool Windows > Flutter Performance.

Flutter performance window

点击 Performance 窗口中的 Show widget rebuild information,查看正在重载的 widget 统计信息和重载频率。右边第二列显示了所在框架的重载次数。如果重载次数过多,会显示一个黄色旋转圆圈。最右一列显示了进入当前页面后 widget 的重载次数。对于未重载的小部件,将显示一个灰色圆圈,否则将显示一个灰色旋转圆圈。

To see the stats about which widgets are being rebuilt, and how often, click Show widget rebuild information in the Performance pane. The exact count of the rebuilds for this frame displays in the second column from the right. For a high number of rebuilds, a yellow spinning circle displays. The column to the far right shows how many times a widget was rebuilt since entering the current screen. For widgets that aren’t rebuilt, a solid grey circle displays. Otherwise, a grey spinning circle displays.

该功能的目的是让你了解 widget 是何时重载的,只看代码的话可能不好发现。如果 widget 在你预想不到的情况下发生了重载,说明你可能需要重构代码,将大型的构建方法拆分成多个 widget。

The purpose of this feature is to make you aware when widgets are rebuilding—you might not realize that this is happening when just looking at the code. If widgets are rebuilding that you didn’t expect, it’s probably a sign that you should refactor your code by splitting up large build methods into multiple widgets.

该工具可以帮助你调试至少四个常见的性能问题:

This tool can help you debug at least four common performance issues:

  1. 整个屏幕(或大部分屏幕)由一个 StatefulWidget 构成,导致不必要的 UI 构建。可将 UI 拆分成多个具有较轻量 build() 方法的 widget。

    The whole screen (or large pieces of it) are built by a single StatefulWidget, causing unnecessary UI building. Split up the UI into smaller widgets with smaller build() functions.

  2. 未在屏幕上显示的 widget 发生了重载。例如,一个延伸到屏幕外的 ListView,或者未给延伸到屏幕外的列表设置 RepaintBoundary,会导致重绘整个列表。

    Offscreen widgets are being rebuilt. This can happen, for example, when a ListView is nested in a tall Column that extends offscreen. Or when the RepaintBoundary is not set for a list that extends offscreen, causing the whole list to be redrawn.

  3. AnimatedBuilder 的 build() 方法绘制了一个不需要动画的子树,导致不必要的静态对象重载。

    The build() function for an AnimatedBuilder draws a subtree that does not need to be animated, causing unnecessary rebuilds of static objects.

  4. 一个 Opacity widget 在 widget tree 中使用了一个不必要的高度,或者通过直接操作 Opacity widget 的透明属性创建 Opacity 动画,导致 widget 和它的子树重载。

    An Opacity widget is placed unnecessarily high in the widget tree. Or, an Opacity animation is created by directly manipulating the opacity property of the Opacity widget, causing the widget itself and its subtree to rebuild.

你可以点击表格中的一行,定位到创建指定 widget 的源码位置。随着代码的运行,旋转图标也会在代码窗口中显示,以帮助你观察正在进行的重载。

You can click on a line in the table to navigate to the line in the source where the widget is created. As the code runs, the spinning icons also display in the code pane to help you visualize which rebuilds are happening.

大量的重载并不一定表示存在问题。通常情况下,只有当你通过分析发现性能不理想时,才需要考虑过度重载的问题。

Note that numerous rebuilds doesn’t necessarily indicate a problem. Typically you should only worry about excessive rebuilds if you have already run the app in profile mode and verified that the performance is not what you want.

记住,widget 的重载信息只在 debug 版本中可用,在真机上使用分析构建 (profile build) 进行应用性能分析,使用调试构建 (debug build) 进行性能问题调试。

And remember, the widget rebuild information is only available in a debug build. Test the app’s performance on a real device in a profile build, but debug performance issues in a debug build.

Flutter 代码编辑提示

Editing tips for Flutter code

如果你有其他我们应该提供的代码提示建议,请 告诉我们!

If you have additional tips we should share, let us know!

代码辅助和快速修复

Assists & quick fixes

代码辅助功能是特定代码标识符相关的代码修改。当光标放在 Flutter widget 上时,黄色灯泡图标会指示可用的修改,可以通过点击灯泡进行修改,或使用键盘快捷键(在 Linux 和 Windows 上使用 Alt+Enter,在 macOS 上使用 Option+Return),如下图所示:

Assists are code changes related to a certain code identifier. A number of these are available when the cursor is placed on a Flutter widget identifier, as indicated by the yellow lightbulb icon. The assist can be invoked by clicking the lightbulb, or by using the keyboard shortcut (Alt+Enter on Linux and Windows, Option+Return on macOS), as illustrated here:

IntelliJ editing assists

Quick Fixes 快速修复功能也是类似的,当一段代码存在错误时,它会出现并帮助纠正错误。它使用红色灯泡表示。

Quick Fixes are similar, only they are shown with a piece of code has an error and they can assist in correcting it. They are indicated with a red lightbulb.

Widget 嵌套辅助

Wrap with new widget assist

当你有一个 widget 需要嵌套在其他 widget 时,可以使用该功能。例如,需要将 widget 嵌套在 RowColumn 中。

This can be used when you have a widget that you want to wrap in a surrounding widget, for example if you want to wrap a widget in a Row or Column.

Widget 列表嵌套辅助

Wrap widget list with new widget assist

和上面的辅助类似,但它嵌套的是一个 widget 的列表,而不是单个的 widget。

Similar to the assist above, but for wrapping an existing list of widgets rather than an individual widget.

child 和 children 转换辅助

Convert child to children assist

将 child 转换成 children,并且把参数值写进一个 list。

Changes a child argument to a children argument, and wraps the argument value in a list.

实时模板

Live templates

实时模板用于增加典型代码结构的输入速度。输入前缀后,在代码完成窗口中选择它:

Live templates can be used to speed up entering typical code structures. They are invoked by typing their prefix, and then selecting it in the code completion window:

IntelliJ live templates

Flutter 插件包含了以下模板:

The Flutter plugin includes the following templates:

你还可以通过 Settings > Editor > Live Templates 定义自定义模板。

You can also define custom templates in Settings > Editor > Live Templates.

键盘快捷键

Keyboard shortcuts

热重载

Hot reload

在 Linux(映射方案默认为 XWin)和 Windows 上,快捷键是 Control+Alt+;Control+Backslash

On Linux (keymap Default for XWin) and Windows the keyboard shortcuts are Control+Alt+; and Control+Backslash.

在 macOS 上(映射方案 Mac OS X 10.5+)上,快捷键是 Command+OptionCommand+Backslash

On macOS (keymap Mac OS X 10.5+ copy) the keyboard shortcuts are Command+Option and Command+Backslash.

可以在 IDE 的设置中修改快捷键:选择 Keymap 后,在右上角的搜索框输入 flutter。右键点击你想修改的快捷键,点击 Add Keyboard Shortcut

Keyboard mappings can be changed in the IDE Preferences/Settings: Select Keymap, then enter flutter into the search box in the upper right corner. Right click the binding you want to change and Add Keyboard Shortcut.

IntelliJ settings keymap

热重载和热重启

Hot reload vs. hot restart

热重载的工作原理是将更新后的代码注入 Dart VM(虚拟机)。不仅包括添加新类,还包括添加方法和字段到已有的类中。但有些类型的代码是无法被热重载的:

Hot reload works by injecting updated source code files into the running Dart VM (Virtual Machine). This includes not only adding new classes, but also adding methods and fields to existing classes, and changing existing functions. A few types of code changes cannot be hot reloaded though:

对于这些更改,你无需结束调试过程而直接热重启 (hot restart) 你的应用:不要点击 Stop 按钮,只需点击 Run 按钮(在运行中),或 Debug 按钮(在调试中),或者按住 Shift 键点击热重载按钮。

For these changes you can fully restart your application, without having to end your debugging session. To perform a hot restart, don’t click the Stop button, simply re-click the Run button (if in a run session) or Debug button (if in a debug session), or shift-click the ‘hot reload’ button.

在 Android Studio 中编辑 Android 代码,并获得完整 IDE 支持

Editing Android code in Android Studio with full IDE support

打开 Flutter 项目的根目录,并不会在 IDE 中显示所有的 Android 文件。 Flutter 应用包含了一个名为 android 的子目录,如果你在 Android Studio 中将该目录作为单独的项目打开,则 IDE 将可以完全支持编辑和重构所有的 Android 文件(比如 Gradle 脚本文件)。

Opening the root directory of a Flutter project doesn’t expose all the Android files to the IDE. Flutter apps contain a subdirectory named android. If you open this subdirectory as its own separate project in Android Studio, the IDE will be able to fully support editing and refactoring all Android files (like Gradle scripts).

如果你已经在 Android Studio 中将整个项目作为 Flutter 应用打开,则有两种方法可以打开 Android 文件,在 IDE 中进行编辑。在进行操作之前,请确保你使用的是最新版本的 Android Studio 和 Flutter 插件。

If you already have the entire project opened as a Flutter app in Android Studio, there are two equivalent ways to open the Android files on their own for editing in the IDE. Before trying this, make sure that you’re on the latest version of Android Studio and the Flutter plugins.

这两种方法,Android Studio 都允许你选择使用单独的窗口,或替换现有窗口打开新项目,两种都是可以的。

For both options, Android Studio gives you the option to use separate windows or to replace the existing window with the new project when opening a second project. Either option is fine.

如果你还没在 Android Studio 中打开 Flutter 项目,你可以一开始就将 Android 文件作为项目打开:

If you don’t already have the Flutter project opened in Android studio, you can open the Android files as their own project from the start:

  1. 点击欢迎窗口中的 Open an existing Android Studio Project。如果 Android Studio 已打开,也可以点击 File > Open

    Click Open an existing Android Studio Project on the Welcome splash screen, or File > Open if Android Studio is already open.

  2. 打开 flutter 应用根目录下的 android 子目录。例如,项目名为 flutter_app,则打开 flutter_app/android

    Open the android subdirectory immediately under the flutter app root. For example if the project is called flutter_app, open flutter_app/android.

如果你还未运行过你的 Flutter 应用,可能会在打开 android 项目时,看到 Android Studio 构建失败的报告。运行项目根目录的 flutter pub get,并通过点击 Build > Make 重建项目,可修复该问题。

If you haven’t run your Flutter app yet, you might see Android Studio report a build error when you open the android project. Run flutter pub get in the app’s root directory and rebuild the project by selecting Build > Make to fix it.

在 IntelliJ IDEA 中编辑 Android 代码

Editing Android code in IntelliJ IDEA

要在 IntelliJ IDEA 中编辑 Android 代码,你需要配置 Android SDK 的位置:

To enable editing of Android code in IntelliJ IDEA, you need to configure the location of the Android SDK:

  1. Preferences > Plugins 中,启用 Android Support

    In Preferences > Plugins, enable Android Support if you haven’t already.

  2. 在项目视图中,右键点击 android 文件夹,然后选择 Open Module Settings

    Right-click the android folder in the Project view, and select Open Module Settings.

  3. Sources 选项中,找到 Language level,并选择 level 8 或更高级别。

    In the Sources tab, locate the Language level field, and select level 8 or later.

  4. Dependencies 选项中,找到 Module SDK,并选择一个 Android SDK。如果这里没有列出 SDK,点击 New 并指定 Android SDK 的位置。确保选择和 Flutter 使用相匹配的 Android SDK(如 flutter doctor 中所示)。

    In the Dependencies tab, locate the Module SDK field, and select an Android SDK. If no SDK is listed, click New and specify the location of the Android SDK. Make sure to select an Android SDK matching the one used by Flutter (as reported by flutter doctor).

  5. 点击 OK

    Click OK.

提示和技巧

Tips and tricks

故障排除

Troubleshooting

已知问题和反馈

Known issues and feedback

Flutter 插件 README 文件中记录了可能影响你使用体验的已知重要问题。

Important known issues that might impact your experience are documented in the Flutter plugin README file.

所有已知问题都会在问题跟踪器中进行跟踪:

All known bugs are tracked in the issue trackers:

我们欢迎所有的错误、问题以及功能反馈。在提交新问题前:

We welcome feedback, both on bugs/issues and feature requests. Prior to filing new issues:

当你在提交新的 issue 时,确保带上运行了 flutter doctor 命令之后的返回内容。

When filing new issues, include the output of flutter doctor.

在 VS Code 里开发 Flutter 应用

目录

安装和配置

Installation and setup

根据 编辑工具设定 的指引来安装 Dart 和 Flutter 扩展(也叫做插件)。

Follow the Set up an editor instructions to install the Dart and Flutter extensions (also called plugins).

更新扩展程序

Updating the extension

扩展的更新会定期发布。默认情况下,当有可用的更新时 VS Code 会自动更新扩展。

Updates to the extensions are shipped on a regular basis. By default, VS Code automatically updates extensions when updates are available.

手动安装更新:

To install updates manually:

  1. 点击侧边栏的 Extensions 按钮。

    Click the Extensions button in the Side Bar.

  2. 如果 Flutter 扩展显示有可用更新,点击更新按钮,然后重载。

    If the Flutter extension is shown with an available update, click the update button and then the reload button.

  3. 重启 VS Code。

    Restart VS Code.

创建项目

Creating projects

有几种方式创建一个新项目。

There are a couple ways to create a new project.

新建项目

Creating a new project

通过 Flutter 入门应用模板新建 Flutter 项目:

To create a new Flutter project from the Flutter starter app template:

  1. 打开命令面板(Ctrl+Shift+P (macOS 用 Cmd+Shift+P))。

    Open the Command Palette (Ctrl+Shift+P (Cmd+Shift+P on macOS)).

  2. 选择 Flutter: New Project 命令然后按 Enter

    Select the Flutter: New Project command and press Enter.

  3. 选择 Application 然后按 Enter

    Select Application and press Enter.

  4. 选择 项目地址

    Select a Project location.

  5. 输入你想要的 项目名

    Enter your desired Project name.

从现有源代码打开项目

Opening a project from existing source code

打开现有 Flutter 项目:

To open an existing Flutter project:

  1. 在 IDE 主窗口点击 File > Open

    Click File > Open from the main IDE window.

  2. 选择存放现有 Flutter 源代码文件的目录。

    Browse to the directory holding your existing Flutter source code files.

  3. 点击 Open

    Click Open.

编写代码及查看问题

Editing code and viewing issues

Flutter 扩展执行代码分析,它提供:

The Flutter extension performs code analysis that enables the following:

运行和调试

Running and debugging

在 IDE 主窗口点击 Run > Start Debugging 或按 F5 开启调试。

Start debugging by clicking Run > Start Debugging from the main IDE window, or press F5.

选择目标设备

Selecting a target device

当一个 Flutter 项目在 VS Code 中打开,你会在状态栏看到一些 Flutter 特有项,包括 Flutter SDK 版本和设备名称(或者无设备信息):

When a Flutter project is open in VS Code, you should see a set of Flutter specific entries in the status bar, including a Flutter SDK version and a device name (or the message No Devices):

VS Code status bar

Flutter 扩展会自动选择上次连接的设备。然而,如果你有多个设备/模拟器连接,点击状态栏的 device 查看屏幕顶部的选择列表。选择你要用来运行或调试的设备。

The Flutter extension automatically selects the last device connected. However, if you have multiple devices/simulators connected, click device in the status bar to see a pick-list at the top of the screen. Select the device you want to use for running or debugging.

无断点运行

Run app without breakpoints

  1. 在 IDE 主窗口点击 Run > Start Without Debugging,或者按 Ctrl+F5,状态栏变橙色说明你正处于调试模式。
    Debug console

    Click Run > Start Without Debugging in the main IDE window, or press Ctrl+F5. The status bar turns orange to show you are in a debug session.
    Debug console

断点运行

Run app with breakpoints

  1. 如果需要,在源代码中设置断点。

    If desired, set breakpoints in your source code.

  2. 在 IDE 主窗口点击 Run > Start Debugging 或按 F5

    Click Run > Start Debugging in the main IDE window, or press F5.

    • 左侧的调试侧边栏显示堆栈帧和变量。

      The left Debug Sidebar shows stack frames and variables.

    • 底部的调试控制台面板显示输出的日志详情。

      The bottom Debug Console pane shows detailed logging output.

    • 调试基于默认的配置。也可以通过点击调试侧边栏顶部的齿轮创建 launch.json 文件自定义调试。你可以修改里面的值。

      Debugging is based on a default launch configuration. To customize, click the cog at the top of the Debug Sidebar to create a launch.json file. You can then modify the values.

以调试 (debug)、性能 (profile) 或发布 (release) 模式运行应用

Run app in debug, profile, or release mode

Flutter 提供了很多种不同的构建模式运行你的应用,更多内容请参考文档 Flutter 的构建模式

Flutter offers many different build modes to run your app in. You can read more about them in Flutter’s build modes.

  1. 打开 VS Code 里的 launch.json 文件

    Open the launch.json file in VS Code.

    如果你没有 launch.json 文件,请到 VS Code 的 Run 视图,点击 create a launch.json file 创建。

    If you do not have a launch.json file, go to the Run view in VS Code and click create a launch.json file.

  2. configurations 部分,修改 flutterMode 属性值为你想要的构建模式即可。

    In the configurations section, change the flutterMode property to the build mode you want to target.

    • 举个例子,如果你希望在调试模式下运行,你的 launch.json 文件应该类似下面这样:

      For example, if you want to run in debug mode, your launch.json might look like this:

      "configurations": [
       {
         "name": "Flutter",
         "request": "launch",
         "type": "dart",
         "flutterMode": "debug"
       }
     ]
    
  3. Run 视图里运行你的应用。

    Run the app through the Run view.

快速编辑和刷新开发周期

Fast edit and refresh development cycle

Flutter 提供一流的开发周期,通过 Stateful Hot Reload 特性使你在几乎修改代码的同时就能看到变化。详情请看 使用热重载

Flutter offers a best-in-class developer cycle enabling you to see the effect of your changes almost instantly with the Stateful Hot Reload feature. See Using hot reload for details.

进阶调试

Advanced debugging

以下的调试指南可能会对你有帮助:

You might find the following advanced debugging tips useful:

可视化布局问题调试

Debugging visual layout issues

在调试会话期间,命令面板Flutter inspector 会添加一些额外的调试命令,包括:

During a debug session, several additional debugging commands are added to the Command Palette and to the Flutter inspector. When space is limited, the icon is used as the visual version of the label.

切换 Baseline 绘制Toggle Baseline Painting Baseline painting icon

每个 RenderBox 在底部绘制一条线。

Causes each RenderBox to paint a line at each of its baselines.

切换重绘 RainbowToggle Repaint Rainbow Repaint rainbow icon

重新绘制时在图层上改变颜色。

Shows rotating colors on layers when repainting.

切换慢模式横幅Toggle Slow Animations Slow animations icon
减慢动画以启用视觉检查。Slows down animations to enable visual inspection.
切换 debug 模式横幅显示Toggle Debug Mode Banner Debug mode banner icon

在运行调试构建时隐藏 debug 模式的横幅 (banner)。

Hides the debug mode banner even when running a debug build.

调试外部库

Debugging external libraries

默认情况下,Flutter 扩展禁止调试外部库。启用步骤:

By default, debugging an external library is disabled in the Flutter extension. To enable:

  1. 选择 Settings > Extensions > Dart Configuration

    Select Settings > Extensions > Dart Configuration.

  2. 勾选 Debug External Libraries 选项。

    Check the Debug External Libraries option.

Flutter 代码编辑提示

Editing tips for Flutter code

如果你有其他我们应该提供的代码提示建议,请 告诉我们

If you have additional tips we should share, let us know!

代码辅助和快速修复

Assists & quick fixes

代码辅助功能是特定代码标识符相关的代码修改。当光标放在 Flutter widget 上时,黄色灯泡图标会指示可用的修改,可以通过点击灯泡进行修改,或者使用快捷键 Ctrl+. (macOS 用 Cmd+.),如图所示:

Assists are code changes related to a certain code identifier. A number of these are available when the cursor is placed on a Flutter widget identifier, as indicated by the yellow lightbulb icon. The assist can be invoked by clicking the lightbulb, or by using the keyboard shortcut Ctrl+. (Cmd+. on Mac), as illustrated here:

Code assists

快速修复跟辅助类似,当一段代码有错误并且可以辅助修正时才会显示。

Quick fixes are similar, only they are shown with a piece of code has an error and they can assist in correcting it.

Widget 嵌套辅助

Wrap with new widget assist

当你有个 widget 想包装进一个容器 widget 时,例如你想把 widget 放入一个 Row 或者 Column

This can be used when you have a widget that you want to wrap in a surrounding widget, for example if you want to wrap a widget in a Row or Column.

Widget 列表嵌套辅助

Wrap widget list with new widget assist

和上面的辅助类似,但它嵌套的是一个 widget 的列表,而不是单个的 widget。

Similar to the assist above, but for wrapping an existing list of widgets rather than an individual widget.

child 和 children 转换辅助

Convert child to children assist

将 child 转换成 children,并且把参数值写进一个 list。

Changes a child argument to a children argument, and wraps the argument value in a list.

StatelessWidget 到 StatefulWidget 的转换
创建 State 类并将代码移动过去,可以将 StatelessWidget 的实现更改为 StatefulWidget

Convert StatelessWidget to StatefulWidget assist
Changes the implementation of a StatelessWidget to that of a StatefulWidget, by creating the State class and moving the code there.

代码片段

Snippets

代码片段可以用来加速输入通用类型代码段。通过输入前缀来调用,然后从代码完成窗口中选择:

Snippets can be used to speed up entering typical code structures. They are invoked by typing their prefix, and then selecting from the code completion window:

Snippets

Flutter 扩展包含以下片段:

The Flutter extension includes the following snippets:

你也可以通过在 命令面板 执行 Configure User Snippets 来自定义片段。

You can also define custom snippets by executing Configure User Snippets from the Command Palette.

键盘快捷键

Keyboard shortcuts

热重载
调试期间,在 调试工具栏 点击 热重载 (Hot Reload) 按钮,或者按 Ctrl+F5(macOS 用 Cmd+F5)执行热重载。

Hot reload
During a debug session, clicking the Hot Reload button on the Debug Toolbar, or pressing Ctrl+F5 (Cmd+F5 on macOS) performs a hot reload.

键位绑定可以在 命令面板 中使用 Open Keyboard Shortcuts 命令进行调整。

Keyboard mappings can be changed by executing the Open Keyboard Shortcuts command from the Command Palette.

热重载和热重启

Hot reload vs. hot restart

热重载的工作原理是将更新后的代码注入 Dart VM(虚拟机)。不仅包括添加新类,还包括添加方法和字段到已有的类中。但有些类型的代码是无法被热重载的:

Hot reload works by injecting updated source code files into the running Dart VM (Virtual Machine). This includes not only adding new classes, but also adding methods and fields to existing classes, and changing existing functions. A few types of code changes cannot be hot reloaded though:

对于这些更改,你无需结束调试过程而直接热重启 (hot restart) 你的应用。要执行热重启,执行 命令面板Flutter:热重启命令,或者按 ``Ctrl+Shift+F5 (在 macOS 上使用 Cmd+Shift+F5)。

For these changes, fully restart your application without having to end your debugging session. To perform a hot restart, run the Flutter: Hot Restart command from the Command Palette, or press Ctrl+Shift+F5(Cmd+Shift+F5 on macOS).

故障排除

Troubleshooting

已知问题和反馈

Known issues and feedback

所有已知 bug 在这个 issue 列表中记录: Dart 和 Flutter 扩展 GitHub issue 追踪

All known bugs are tracked in the issue tracker: Dart and Flutter extensions GitHub issue tracker.

我们非常欢迎 bugs/issues 和特性请求的反馈。在提交新 issue 之前:

We welcome feedback, both on bugs/issues and feature requests. Prior to filing new issues:

提交新 issue 时,请包含 flutter doctor 输出。

When filing new issues, include flutter doctor output.

开发者工具概览

目录

开发工具是什么?

What is DevTools?

开发工具是一套 Dart 和 Flutter 的性能调试工具。目前已经“行进”到 Beta 版本了,但仍在正在持续开发中。

DevTools is a suite of performance and debugging tools for Dart and Flutter.

Dart DevTools Screens

我可以用开发工具来做什么?

What can I do with DevTools?

下面列出了一些可以用开发工具来实现的操作:

Here are some of the things you can do with DevTools:

我们希望您将开发工具与现有的 IDE 或基于命令行的开发流程结合起来使用。

We expect you to use DevTools in conjunction with your existing IDE or command-line based development workflow.

如何安装开发工具?

How do I install DevTools?

查看 Android Studio/IntelliJVS Code命令行 页面获取更多安装指导。

See the Android Studio/IntelliJ, VS Code, or command line pages for installation instructions.

提交反馈

Providing feedback

请在 开发者工具 issue 追踪器 中尝试使用开发工具,并提交反馈和文件 issue。

Please give DevTools a try, provide feedback, and file issues in the DevTools issue tracker. Thanks!

其他资源

Other resources

关于调试、分析 Flutter 应用程序的更多详细,请查阅 调试 页面,尤其是 其他资源 列表。

For more information on debugging and profiling Flutter apps, see the Debugging page and, in particular, its list of other resources.

如果你希望知道更多如何在命令行下使用开发者工具 (DevTools) 的话,请参考这个页面 Dart 开发者工具.

For more information on using DevTools with Dart command-line apps, see the DevTools documentation on dart.dev.

在 Android Studio 上安装和运行开发者工具

目录

安装 Flutter 插件

Install the Flutter plugin

请在第一次使用前安装 Flutter 插件,它可以在 Intellij 的 Plugins 或 Android Studio 的设置中开启,页面打开后便可以在 marketPlace 中找到 Flutter 插件。

Install the Flutter plugin if you don’t already have it installed. This can be done using the normal Plugins page in the IntelliJ and Android Studio settings. Once that page is open, you can search the marketplace for the Flutter plugin.

开始调试一个应用

Start an app to debug

需要在启动调试工具前开启一个 Flutter 应用,它可以通过打开 Flutter 项目实现,首先要确保你已经完成设备的连接,然后只需点击工具栏的 RunDebug 按钮。

To open DevTools, you first need to run a Flutter app. This can be accomplished by opening a Flutter project, ensuring that you have a device connected, and clicking the Run or Debug toolbar buttons.

从工具栏/菜单启动调试工具

Launch DevTools from the toolbar/menu

应用启动后,你可以通过以下几种方式运行调试工具:

Once an app is running, you can start DevTools using one of the following:

screenshot of Open DevTools button

从事件 (action) 中启动调试工具

Launch DevTools from an action

你同样可以在 IntelliJ 中运行调试工具,先开启 Find Action… 对话框(Mac 上可以同时按下 ‘Command+Shift+A’),然后查找 Open DevTools 选项。在调试工具已安装的前提下,相关服务会启动,同时打开指向待调试应用的浏览器实例。

You can also open DevTools from an IntelliJ action. Open the Find Action… dialog (on a Mac, press Command+Shift+A), and search for the Open DevTools action. When you select that action, DevTools is installed (if it isn’t already), the DevTools server launches, and a browser instance opens pointing to the DevTools app.

调试工具在 IntelliJ 事件启动时并不会直接连接到 Flutter 应用,因此当前应用的服务协议端口是必须的,你可以在 Connect to a running app 的对话框中开启。

When opened with an IntelliJ action, DevTools is not connected to a Flutter app. You’ll need to provide a service protocol port for a currently running app. You can do this using the inline Connect to a running app dialog.

在 VS Code 里安装和使用开发者工具

目录

安装 VS Code 插件

Install the VS Code extensions

如果你想在 VS Code 中使用开发工具,你就一定需要安装 Dart 扩展。如果你还想要调试 Flutter 应用程序,那你还应该安装 Flutter 扩展

To use the DevTools from VS Code, you need the Dart extension. If you’re debugging Flutter applications, you should also install the Flutter extension.

进行调试应用程序

Start an application to debug

通过在 VS Code 中打开你的项目的根目录(包含 pubspec.yaml)并点击 Run > Debugging (F5),来开启调试会话。

Start a debug session for your application by opening the root folder of your project (the one containing pubspec.yaml) in VS Code and clicking Run > Start Debugging (F5).

启动开发工具

Launch DevTools

一旦调试会话处于活跃且应用程序已开启,那么 VS Code 命令控制板中将会显示 Dart: Open DevTools

Once the debug session is active and the application has started, the Dart: Open DevTools command becomes available in the VS Code command palette:

Screenshot showing Open DevTools command

当你第一次运行时(以及未来更新开发工具包时),系统会提醒你激活或升级开发工具。

The first time you run this (and subsequently when the DevTools package is updated), you are prompted to activate or upgrade DevTools.

Screenshot showing Active DevTools command

接下来,开发工具将会在浏览器中启动,并自动连接至你的调试会话。

Clicking the Open button uses pub global activate to activate the DevTools package for you. Next, DevTools launches in your browser and automatically connects to your debug session.

Screenshot showing DevTools in a browser

当开发工具激活后,你将可以在 VS Code 的状态栏中看到它们。如果你已关闭浏览器选项卡,只要还有可用的 Dart/Flutter 调试会话,你也可以通过单击状态栏来重新启动浏览器。

While DevTools is active, you’ll see them in the status bar of VS Code. If you’ve closed the browser tab, you can click the status bar to re-launch your browser, so long as there’s still a suitable Dart/Flutter debugging session available.

Screenshot showing DevTools in the VS Code status bar

通过命令行安装和运行开发者工具

目录

安装开发者工具

Install DevTools

如果在你的环境变量 PATH 中有 dart, 可以运行:

If you have dart on your path, you can run the following command:

dart pub global activate devtools

如果在你的环境变量 PATH 中有 flutter, 可以运行:

If you have flutter on your path, you can run the following:

flutter pub global activate devtools

这个命令会在你的机器上安装(或升级)开发者工具。

That command installs (or updates) DevTools on your machine.

启动开发者工具服务

Launch the DevTools application server

下一步,启动本地 web server 服务来运行开发者工具。运行下面两个命令中的一个。

Next, run the local web server, which serves the DevTools application itself. To do that, run one of the following two commands:

dart pub global run devtools   # If you have `dart` on your path.

或者

OR

flutter pub global run devtools   # If you have `flutter` on your path.

On the command line, you should see output that looks something like:

Serving DevTools at http://127.0.0.1:9100

启动一个 app 来 debug

Start an application to debug

下一步,启动并连接一个 app。可以是 Flutter app 或者一个 Dart 命令行应用。下面这个命令是启动一个 Flutter app:

Next, start an app to connect to. This can be either a Flutter application or a Dart command-line application. The command below specifies a Flutter app:

cd path/to/flutter/app
flutter run

运行 flutter run 时,你需要连接一个设备或者模拟器。当 app 启动后,你会在命令行中看到如下内容:

You need to have a device connected, or a simulator open, for flutter run to work. Once the app starts, you’ll see a message in your terminal that looks like the following:

An Observatory debugger and profiler on iPhone X is available
at: http://127.0.0.1:50976/Swm0bjIe0ks=/

记下这个 URL ,待会儿你可以使用它来连接 app 和开发者工具。

Keep note of this URL, as you will use it to connect your app to DevTools.

打开开发者工具并且连接到目标 app

Open DevTools and connect to the target app

上述完成后,使用开发者工具就会很简单,只需打开 chrome 并访问 http://localhost:9100

Once it’s set up, using DevTools is as simple as opening a Chrome browser window and navigating to http://localhost:9100.

当这个网页打开后,你会看到一个链接对话框:

Once DevTools opens, you should see a connect dialog:

Screenshot of a logging view

将从启动 app 时获得的 URL 链接(在这个例子里是 http://127.0.0.1:50976/Swm0bjIe0ks=/ )复制到这个链接对话框中来把你的 app 和开发者工具链接起来。

Paste the URL you got from running your app (in this example, http://127.0.0.1:50976/Swm0bjIe0ks=/) into the connect dialog to connect your app to DevTools.

这个链接包含一个秘钥 token,所以每次启动你的 app 时,链接都会改变。这意味着如果重启 app 后,你需要用新的 URL 链接来连接开发者工具。

This URL contains a security token, so it’s different for each run of your app. This means that if you stop your application and re-run it, you need to connect to DevTools with the new URL.

使用 Flutter inspector 工具

目录

这是什么?

What is it?

Flutter widget inspector 是一个强大的工具,用于可视化和查看 widget 树。 Flutter 框架层使用 widgets 作为 核心构建模块 来处理从控件(例如文本、按钮和切换等)到布局(例如居中、填充、行和列等)的所有内容。 Flutter inspector 不仅可以帮助你可视化查看 Flutter widget 树,还有其他的作用:

The Flutter widget inspector is a powerful tool for visualizing and exploring Flutter widget trees. The Flutter framework uses widgets as the core building block for anything from controls (such as text, buttons, and toggles), to layout (such as centering, padding, rows, and columns). The inspector helps you visualize and explore Flutter widget trees, and can be used for the following:

Screenshot of the Flutter inspector window

开始使用

Get started

要调试布局问题,请在 Debug 模式 下运行应用程序,然后点击 DevTools 工具栏上的 Flutter inspector 选项打开调试面板。

To debug a layout issue, run the app in debug mode and open the inspector by clicking the Flutter Inspector tab on the DevTools toolbar.

可视化地调试布局问题

Debugging layout issues visually

下面是 Flutter inspector 工具栏中可用功能的指南。当空间有限时,将直接使用图标展示。

The following is a guide to the features available in the inspector’s toolbar. When space is limited, the icon is used as the visual version of the label.

Select widget mode icon 选择 widget 模式

Select widget mode icon Select widget mode

启用此按钮以在设备上选择 widget 进行查看。有关更多信息,请参考 查看 widget

Enable this button in order to select a widget on the device to inspect it. For more information, see Inspecting a widget.

Refresh tree icon 刷新树

Refresh tree icon Refresh tree

重新加载当前 widget 的信息。

Reload the current widget info

Slow animations icon 慢速动画

Slow animations icon Slow Animations

以五分之一的速度运行动画以便对它们进行优化。

Run animations 5 times slower to help fine-tune them.

Show guidelines mode icon 显示引导线

Show guidelines mode icon Show guidelines

覆盖一层引导线以帮助调整布局问题。

Overlay guidelines to assist with fixing layout issues.

Show baselines icon 显示基线

Show baselines icon Show baselines

针对文字对齐展示文字的基线。对检查文字是否对齐有帮助。

Show baselines, which are used for aligning text. Can be useful for checking if text is aligned.

Highlight repaints icon 高亮重绘制内容

Highlight repaints icon Highlight repaints

重新绘制时在图层上依次显示不同的颜色。

Shows rotating colors on layers when repainting.

Highlight oversized images icon 高亮尺寸过大的图片

Highlight oversized images icon Highlight oversized images

在运行的应用程序中高亮并反转消耗过多内存的图像。

Highlights images that are using too much memory by inverting colors and flipping them.

检查一个 widget

Inspecting a widget

你可以浏览 widget 树并查看其附近的 widgets 和它们的属性值。

You can browse the interactive widget tree to view nearby widgets and see their field values.

要在 widget 树中找到单个 UI 元素,请点击工具栏中的 Select Widget Mode 按钮。这将使设备上的应用程序进入「widget select」模式。点击应用界面上的任何 widget,将选中 widget 并将 widget 树滚动到对应的节点。再次点击 Select Widget Mode 按钮则退出「widget select」模式。

To locate individual UI elements in the widget tree, click the Select Widget Mode button in the toolbar. This puts the app on the device into a “widget select” mode. Click any widget in the app’s UI; this selects the widget on the app’s screen, and scrolls the widget tree to the corresponding node. Toggle the Select Widget Mode button again to exit widget select mode.

在调试布局问题时,要查看的关键字段是 sizeconstraints。其中约束沿树结构向下传递,尺寸信息则向上返回。想要了解更多信息,可以查看 深入理解 Flutter 布局约束

When debugging layout issues, the key fields to look at are the size and constraints fields. The constraints flow down the tree, and the sizes flow back up. For more information on how this works, see Understanding constraints.

Flutter 布局浏览器

Flutter Layout Explorer

Flutter 布局浏览器可以帮助你更好地理解 Flutter 布局。

The Flutter Layout Explorer helps you to better understand Flutter layouts.

有关此工具的操作概述,观看 Flutter Explorer 的介绍视频:

For an overview of what you can do with this tool, see the Flutter Explorer video:

下面详细介绍的文章可能对你有帮助:

You might also find the following step-by-step article useful:

使用布局浏览器

Using the Layout Explorer

从 Flutter Inspector 中,选择一个 widget。布局浏览器支持 弹性布局 和固定大小的布局,并且针对它们配备了特定的工具。

From the Flutter Inspector, select a widget. The Layout Explorer supports both flex layouts and fixed size layouts, and has specific tooling for both kinds.

弹性布局

Flex layouts

当你选择了一个弹性布局 widget(例如,RowColumnFlex)或它的子 widget 时,弹性布局工具将显示在布局浏览器中。

When you select a flex widget (for example, Row, Column, Flex) or a direct child of a flex widget, the flex layout tool will appear in the Layout Explorer.

布局浏览器会直观的显示 Flex widgets 及其子元素的布局方式。浏览器中还会显示主轴和交叉轴,以及每个轴当前的对齐方式(例如,start、end 和 spaceBetween)。它还显示了诸如弹性系数、弹性适配和布局约束等详细信息。

The Layout Explorer visualizes how Flex widgets and their children are laid out. The explorer identifies the main axis and cross axis, as well as the current alignment for each (for example, start, end, and spaceBetween). It also shows details like flex factor, flex fit, and layout constraints.

此外,浏览器中还会显示布局约束冲突和渲染溢出错误。正如你在设备上看到的那样,违背布局约束的地方会被标记成红色,溢出错误以标准的「黄色条带」显示。这些可视化的错误是为了让我们更好地理解溢出错误发生的原因,并了解如何修复它们。

Additionally, the explorer shows layout constraint violations and render overflow errors. Violated layout constraints are colored red, and overflow errors are presented in the standard “yellow-tape” pattern, as you might see on a running device. These visualizations aim to improve understanding of why overflow errors occur as well as how to fix them.

The Layout Explorer showing errors and device inspector

Select Widget Mode 模式下,点击布局浏览器中的 widget 会同步选择到设备上。启用此模式,请点击调试面板中的 Select Widget Mode 按钮。

Clicking a widget in the layout explorer mirrors the selection on the on-device inspector. Select Widget Mode needs to be enabled for this. To enable it, click on the Select Widget Mode button in the inspector.

The Select Widget Mode button in the inspector

你可以在布局浏览器的下拉列表修改属性值,例如弹性系数、弹性适配和对齐方式。当修改 widget 的属性时,您会看到新的值同时在浏览器和运行 Flutter 应用程序的设备上生效。浏览器通过动画使更改的效果清晰可见。从布局浏览器中对 widget 属性的更改不会修改源代码,将在热重载时还原。

For some properties, like flex factor, flex fit, and alignment, you can modify the value via dropdown lists in the explorer. When modifying a widget property, you see the new value reflected not only in the Layout Explorer, but also on the device running your Flutter app. The explorer animates on property changes so that the effect of the change is clear. Widget property changes made from the layout explorer don’t modify your source code and are reverted on hot reload.

交互属性
Interactive Properties

布局资源管理器支持修改 mainAxisAlignmentcrossAxisAlignmentFlexParentData.flex。将来,我们可能会添加对其他属性的支持,例如 mainAxisSizetextDirectionFlexParentData.fit.

Layout Explorer supports modifying mainAxisAlignment, crossAxisAlignment, and FlexParentData.flex. In the future, we may add support for additional properties such as mainAxisSize, textDirection, and FlexParentData.fit.

mainAxisAlignment

The Layout Explorer changing main axis alignment

支持属性:

Supported values:

crossAxisAlignment

The Layout Explorer changing cross axis alignment

支持属性:

Supported values:

FlexParentData.flex

The Layout Explorer changing flex factor

布局浏览器支持设置 7 种弹性因子(null、0、1、2、3、4、5),但从技术上讲,弹性 widget 子级的弹性因子可以是任何整数。

Layout Explorer supports 7 flex options in the UI (null, 0, 1, 2, 3, 4, 5), but technically the flex factor of a flex widget’s child can be any int.

Flexible.fit

The Layout Explorer changing fit

布局浏览器支持两种不同类型的 FlexFitloosetight

Layout Explorer supports the two different types of FlexFit: loose and tight.

固定大小布局

Fixed size layouts

当您选择一个固定大小的 widget 而不是弹性 widget 时,它的布局信息将显示在布局浏览器中。你可以看到所选 widget 及其最近的上一级 RenderObject 的大小、约束和填充信息。

When you select a fixed size widget that is not a child of a flex widget, fixed size layout information will appear in the Layout Explorer. You can see size, constraint, and padding information for both the selected widget and its nearest upstream RenderObject.

The Layout Explorer fixed size tool

调试视觉效果

Visual debugging

Flutter Inspector 提供了多种以可视化方式调试应用的方式。以下是在 Flutter DevTools 中的 inspector 可用的选项:

The Flutter Inspector provides several options for visually debugging your app. These are the options available from the inspector within Flutter DevTools.

慢速动画

Slow animations

启用时,动画将以约五分之一的原有速度运行,方便对视觉效果进行检查。当你想要仔细地观察并调试看起来不正常的动画时,这个选项会非常有用。

When enabled, this option runs animations 5 times slower for easier visual inspection. This can be useful if you want to carefully observe and tweak an animation that doesn’t look quite right.

你也可以使用代码设置:

This can also be set in code:

import 'package:flutter/scheduler.dart';

void setSlowAnimations() {
  timeDilation = 5.0;
}

这会让动画时长增加 5 倍(速度减慢 5 倍)。

This slows the animations by 5x.

更多内容

See also

以下的链接提供了更多细节内容。

The following links provide more info.

以下的录屏展示了动画减速前后的对比。

Screen recording showing normal animation speed Screen recording showing slowed animation speed

显示引导线

Show guidelines

该功能会在你的应用顶层绘制引导线,展示绘制区域、对齐、间距、滚动视图、裁剪和空位填充。

This feature draws guidelines over your app that display render boxes, alignments, paddings, scroll views, clippings and spacers.

这个工具能帮助你更加了解你的布局。例如查找不需要的填充或者理解 widget 的对齐方式。

This tool can be used for better understanding your layout. For instance, by finding unwanted padding or understanding widget alignment.

你也可以通过代码启用:

You can also enable this in code:

import 'package:flutter/rendering.dart';

void showLayoutGuidelines() {
  debugPaintSizeEnabled = true;
}

Render boxes

RenderBox

绘制在屏幕上的 widgets 会创建一个 RenderBox,它是 Flutter 布局的基础构建。这些 RenderBox 会加上一个浅蓝色的边框:

Widgets that draw to the screen create a render box, the building blocks of Flutter layouts. They’re shown with a bright blue border:

Screenshot of render box guidelines

对齐方式

Alignments

对齐方式将以黄色箭头展示。这些箭头会显示出垂直和竖屏方向上 widget 相对其父布局的偏移。例如,这个按钮图标有四个箭头表示它被居中展示:

Alignments are shown with yellow arrows. These arrows show the vertical and horizontal offsets of a widget relative to its parent. For example, this button’s icon is shown as being centered by the four arrows:

Screenshot of alignment guidelines

间距

Padding

间距会以半透明的蓝色背景显示:

Padding is shown with a semi-transparent blue background:

Screenshot of padding guidelines

滚动视图

Scroll views

包含滚动内容的 widget(例如 ListView)会展示绿色的箭头:

Widgets with scrolling contents (such as list views) are shown with green arrows:

Screenshot of scroll view guidelines

裁剪

Clipping

使用了诸如 ClipRect Widget 进行裁剪的内容,会以粉红色的虚线加一个剪刀图标展示:

Clipping, for example when using the ClipRect widget, are shown with a dashed pink line with a scissors icon:

Screenshot of clip guidelines

空位填充

Spacers

空位填充的 widgets 会以灰色背景展示,例如没有 child 的 SizedBox

Spacer widgets are shown with a grey background, such as this SizedBox without a child:

Screenshot of spacer guidelines

显示基线

Show baselines

该选项会显示所有的基线。基线是水平的用来定位文字的线。

This option makes all baselines visible. Baselines are horizontal lines used to position text.

在检查文字是否垂直对齐时,基线会非常有用。例如,下图中文字的基线稍微有一些错位:

This can be useful for checking whether text is precisely aligned vertically. For example, the text baselines in the following screenshot are slightly misaligned:

Screenshot with show baselines enabled

Baseline widget 可以用来调整基线。

The Baseline widget can be used to adjust baselines.

在设置了基线的 RenderBox 上,都会显示一条线。字母的基线以绿色展示,而符号的基线以黄色展示。

A line is drawn on any render box that has a baseline set; alphabetic baselines are shown as green and ideographic as yellow.

你也可以通过代码启用:

You can also enable this in code:

import 'package:flutter/rendering.dart';

void showBaselines() {
  debugPaintBaselinesEnabled = true;
}

高亮重绘制内容

Highlight repaints

该选项会为所有的 RenderBox 绘制一层边框,在它们重新绘制时改变颜色。

This option draws a border around all render boxes that changes color every time that box repaints.

以彩虹色谱循环的颜色,有利于你找到应用中频繁重绘导致性能消耗过大的部分。

This rotating rainbow of colors is useful for finding parts of your app that are repainting too often and potentially harming performance.

例如,一个小动画可能会导致整个页面一直在重绘。将动画使用 RepaintBoundary widget 嵌套,可以保证动画只会导致其本身重绘。

For example, one small animation could be causing an entire page to repaint on every frame. Wrapping the animation in a RepaintBoundary widget limits the repainting to just the animation.

下面是一个进度指示器导致其容器重绘的例子:

Here the progress indicator causes its container to repaint:

class EverythingRepaintsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Repaint Example')),
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

Screen recording of a whole screen repainting

将进度指示器使用 RepaintBoundary 包裹,可以将重绘范围缩小至它本身占有的区域。

Wrapping the progress indicator in a RepaintBoundary causes only that section of the screen to repaint:

class AreaRepaintsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Repaint Example')),
      body: Center(
        child: RepaintBoundary(
          child: CircularProgressIndicator(),
        ),
      ),
    );
  }
}

Screen recording of a just a progress indicator repainting

RepaintBoundary widget 也有一些额外的消耗。它们对性能有一定的帮助,但也会在创建额外的绘制画布时增加一定的内存消耗。

RepaintBoundary widgets have tradeoffs. They can help with performance, but they also have an overhead of creating a new canvas, which uses additional memory.

你也可以通过代码启用:

You can also enable this option in code:

import 'package:flutter/rendering.dart';

void highlightRepaints() {
  debugRepaintRainbowEnabled = true;
}

高亮尺寸过大的图片

Highlight oversized images

该选项会将尺寸过大的图片高亮表示,并且进行垂直翻转及色调反转:

This option highlights images that are too large by both inverting their colors and flipping them vertically:

A highlighted oversized image

被高亮的图片使用了过多的内存。例如一张 5MB 大小的图片以 100x100 像素展示。

The highlighted images use more memory than is required; for example, a large 5MB image displayed at 100 by 100 pixels.

这样的图片会导致性能低下,在低端设备上尤为明显,而当你在诸如列表中有大量这样的图片时,性能的下降会叠加。调试控制台窗口中会打印每个图片的信息:

Such images can cause poor performance, especially on lower-end devices and when you have many images, as in a list view, this performance hit can add up. Information about each image is printed in the debug console:

dash.png has a display size of 213×392 but a decode size of 2130×392, which uses an additional 2542KB.

超过 128KB 的图片会被视为过大。

Images are deemed too large if they use at least 128KB more than required.

调整图片

Fixing images

在可能的情况下,最好的办法是调整图片资源的大小,让它变得更小。

Wherever possible, the best way to fix this problem is resizing the image asset file so it’s smaller.

如果该方法不可行,你可以使用 Image 构造里的 cacheHeightcacheWidth 参数:

If this isn’t possible, you can use the cacheHeight and cacheWidth parameters on the Image constructor:

class ResizedImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Image.asset(
      'dash.png',
      cacheHeight: 213,
      cacheWidth: 392,
    );
  }
}

这样的方法可以让引擎以指定的大小解析图片,减少内存的消耗(解析开销和空间占用相较图片调整图片本身仍然较大)。无论如何设置参数,图片依然会以布局限制或大小进行渲染。

This makes the engine decode this image at the specified size, and reduces memory usage (decoding and storage is still more expensive than if the image asset itself was shrunk). The image is rendered to the constraints of the layout or width and height regardless of these parameters.

该属性同样可以使用代码设置:

This property can also be set in code:

import 'package:flutter/painting.dart';

void showOversizedImages() {
  debugInvertOversizedImages = true;
}

更多内容

More information

以下的链接提供了更多细节内容:

You can learn more at the following link:

树的详细信息

Details Tree

选择 Details Tree 标签展示选中 widget 的树结构的详细信息。

Select the Details Tree tab to display the details tree for the selected widget.

The Details Tree tab

从树的详细信息中,你可以获取有关 widget 的属性、渲染对象和子节点等有用信息。

From the details tree, you can gather useful information about a widget’s properties, render object, and children.

The Details Tree view

追踪 widget 创建

Track widget creation

Flutter inspector 的部分功能是基于检测应用程序的源码,以便更好地理解创建 widget 的源位置。 Flutter inspector 可以以类似于在源代码中定义 UI 的方式呈现 widget 树。如果没有它,widget 树中的组成某个节点的树结构会更深,并且更难理解运行时 widget 的层次结构如何与应用程序的 UI 相对应。

Part of the functionality of the Flutter inspector is based on instrumenting the application code in order to better understand the source locations where widgets are created. The source instrumentation allows the Flutter inspector to present the widget tree in a manner similar to how the UI was defined in your source code. Without it, the tree of nodes in the widget tree are much deeper, and it can be more difficult to understand how the runtime widget hierarchy corresponds to your application’s UI.

你可以通过在 flutter run 后面添加参数 -no track widget creation 来禁用此功能。

You can disable this feature by passing --no-track-widget-creation to the flutter run command.

下面是 widget 树在启用和不启用追踪 widget 创建下的示例。

Here are examples of what your widget tree might look like with and without track widget creation enabled.

启用追踪 widget 创建(默认):

Track widget creation enabled (default):

The widget tree with track widget creation enabled

关闭追踪 widget 创建(不推荐):

Track widget creation disabled (not recommended):

The widget tree with track widget creation disabled

此功能可避免在调试构建中将其他相同的 const 的 Widgets 视为相同。有关更多详细信息,请参阅关于 调试时常见问题 的讨论。

This feature prevents otherwise-identical const Widgets from being considered equal in debug builds. For more details, see the discussion on common problems when debugging.

其他资源

Other resources

有关 Flutter inspector 常用功能的演示,请参考 DartConf 2018 talk 中基于 IntelliJ 上的演示。

For a demonstration of what’s generally possible with the inspector, see the DartConf 2018 talk demonstrating the IntelliJ version of the Flutter inspector.

Using the Performance view

Contents

What is it?

The performance view offers timing and performance information for activity in your application. It consists of three parts, each increasing in granularity.

The performance view also supports importing and exporting of data snapshots. For more information, see the Import and export section.

Flutter frames chart

This chart contains Flutter frame information for your application. Each bar set in the chart represents a single Flutter frame. The bars are color-coded to highlight the different portions of work that occur when rendering a Flutter frame: work from the UI thread and work from the raster thread (previously known as the GPU thread).

Screenshot from a performance snapshot

Selecting a bar from this chart centers the flame chart below on the timeline events corresponding to the selected Flutter frame. The events are highlighted with blue brackets.

Screenshot from a timeline recording

UI

The UI thread executes Dart code in the Dart VM. This includes code from your application as well as the Flutter framework. When your app creates and displays a scene, the UI thread creates a layer tree, a lightweight object containing device-agnostic painting commands, and sends the layer tree to the raster thread to be rendered on the device. Do not block this thread.

Raster

The raster thread (previously known as the GPU thread) executes graphics code from the Flutter Engine. This thread takes the layer tree and displays it by talking to the GPU (graphic processing unit). You cannot directly access the raster thread or its data, but if this thread is slow, it’s a result of something you’ve done in the Dart code. Skia, the graphics library, runs on this thread.

Sometimes a scene results in a layer tree that is easy to construct, but expensive to render on the raster thread. In this case, you need to figure out what your code is doing that is causing rendering code to be slow. Specific kinds of workloads are more difficult for the GPU. They might involve unnecessary calls to saveLayer(), intersecting opacities with multiple objects, and clips or shadows in specific situations.

For more information on profiling, see Identifying problems in the GPU graph.

Jank

The frame rendering chart shows jank with a red overlay. A frame is considered to be janky if it takes more than ~16 ms to complete (for 60 FPS devices). To achieve a frame rendering rate of 60 FPS (frames per second), each frame must render in ~16 ms or less. When this target is missed, you may experience UI jank or dropped frames.

For more information on how to analyze your app’s performance, see Flutter performance profiling.

Timeline events chart

The timeline events chart shows all event tracing from your application. The Flutter framework emits timeline events as it works to build frames, draw scenes, and track other activity such as HTTP traffic. These events show up here in the Timeline. You can also send your own Timeline events via the dart:developer Timeline and TimelineTask APIs.

Screenshot of timeline events for a frame

The flame chart supports zooming and panning:

You can click an event to view CPU profiling information in the CPU profiler below, described in the next section.

CPU Profiler

Start recording a CPU profile by clicking Record. When you are done recording, click Stop. At this point, CPU profiling data is pulled from the VM and displayed in the profiler views (Call Tree, Bottom Up, and Flame Chart).

Profile granularity

The default rate at which the VM collects CPU samples is 1 sample / 250 μs. This is selected by default on the CPU profiler view as “Profile granularity: medium”. This rate can be modified via the selector at the top of the page. The sampling rates for low, medium, and high granularity are 1 / 1000 μs, 1 / 250 μs, and 1 / 50 μs, respectively. It is important to know the trade-offs of modifying this setting.

A higher granularity profile has a higher sampling rate, and therefore yields a fine-grained CPU profile with more samples. This may also impact performance of your app since the VM is being interrupted more often to collect samples. This also causes the VM’s CPU sample buffer to overflow more quickly. The VM has limited space where it can store CPU sample information. At a higher sampling rate, the space fills up and begins to overflow sooner than it would have if a lower sampling rate was used. This means that you may not have access to CPU samples from the beginning of the recorded profile.

A lower granularity profile has a lower sampling rate, and therefore yields a coarse-grained CPU profile with fewer samples. However, this impacts your app’s performance less. The VM’s sample buffer also fills more slowly, so you can see CPU samples for a longer period of app run time. This means that you have a better chance of viewing CPU samples from the beginning of the recorded profile.

Flame chart

This tab of the profiler shows CPU samples for the recorded duration. This chart should be viewed as a top-down stack trace, where the top-most stack frame calls the one below it. The width of each stack frame represents the amount of time it consumed the CPU. Stack frames that consume a lot of CPU time may be a good place to look for possible performance improvements.

Screenshot of a flame chart

Call tree

The call tree view shows the method trace for the CPU profile. This table is a top-down representation of the profile, meaning that a method can be expanded to show its callees.

Total time
Time the method spent executing its own code as well as the code for its callees.
Self time
Time the method spent executing only its own code.
Method
Name of the called method.
Source
File path for the method call site.

Screenshot of a call tree table

Bottom up

The bottom up view shows the method trace for the CPU profile but, as the name suggests, it’s a bottom-up representation of the profile. This means that each top-level method in the table is actually the last method in the call stack for a given CPU sample (in other words, it’s the leaf node for the sample).

In this table, a method can be expanded to show its callers.

Total time

Time the method spent executing its own code as well as the code for its callee.

Self time

For top-level methods in the bottom-up tree (leaf stack frames in the profile), this is the time the method spent executing only its own code. For sub nodes (the callers in the CPU profile), this is the self time of the callee when being called by the caller. In the following example, the self time of the caller createRenderObject is equal to the self time of the callee debugCheckHasDirectionality when being called by the caller.

Method

Name of the called method.

Source

File path for the method call site.

Screenshot of a bottom up table

Import and export

DevTools supports importing and exporting performance snapshots. Clicking the export button (upper-right corner above the frame rendering chart) downloads a snapshot of the current data on the performance page. To import a performance snapshot, you can drag and drop the snapshot into DevTools from any page. Note that DevTools only supports importing files that were originally exported from DevTools.

使用内存视图 (Memory view)

目录

这是什么?

What is it?

使用类构造函数创建的 Dart 对象(例如,使用 new MyClass()MyClass())会被分配在称为 的内存区域中。堆中的内存由 Dart VM(虚拟机)管理。

Allocated Dart objects created using a class constructor (for example, by using new MyClass() or MyClass()) live in a portion of memory called the heap. The memory in the heap is managed by the Dart VM (virtual machine).

DevTools 内存分析页面

DevTools memory page

你可以在 DevTools 中的内存分析页面观察在给定的时段内 isolate 在怎样使用内存。

DevTools Memory page lets you peek at how an isolate is using memory at a given moment.

DevTools 中的内存分析包括 3 个主要功能:

Memory profiling in DevTools consists of 3 main functions:

绘制内存统计信息和事件图表

Charting memory statistics and events

当选中顶部 tab 中的 Memory(内存)选项时,DevTools 将收集来自 VM 的内存统计数据。这些统计数据显示在两个概览图(Dart 内存以及 Android 图表)中,记录了一般内存的使用情况,例如使用的总堆、外部堆、最大堆容量、常驻集大小(RSS)。当你与应用程序交互时,会触发各种事件,例如内存 GC(垃圾回收)、Flutter 事件、用户触发的事件(使用 dart:developer 包)与内存统计数据会被记录在同一时间线中。所有这些收集的统计数据和事件都显示在图表中,请参见 内存剖析

At the top-level, when the memory tab is selected memory statistics from the VM are collected. These statistics are displayed in the two overview charts (Dart memory and Android-only) the collection of general memory usage e.g., total heap used, external heap, maximum heap capacity, Resident Set Size (RSS). As you interact with your application various events are detected e.g., memory GC (garabage collection),Flutter events, user fired events (using the dart:developer package) are collected in the same timeline as the memory statistics. All of this collected statistics and events are displayed in charts see Memory anatomy.

分析和快照

Analysis and snapshots

快照是 Dart 内存堆中所有对象最复杂和最耗时的完整视图。每次保存快照时,都会对收集的内存数据进行分析。该分析会尝试识别任何可能导致应用程序内存泄漏或崩溃的内存模式。例如,为缩略图大小的图片加载大型资源是低效的,可以通过加载较小的资源或调整 cacheWidth/cacheHeight,减小图片的解码大小,降低 ImageCache 的内存使用,以此提高内存使用率。你可以在 Analysis 选项 中查看分析捕捉到的问题。

A Snapshot is a complete, the most complex and time consuming view of all objects in the Dart memory heap. Each time a snapshot is taken, an analysis is performed over the collected memory data. The analysis attempts to identify any memory patterns that may cause leaks or lead to application crashes. For example, loading large assets for thumbnail-sized images inefficiently, memory usage can be improved by loading smaller assets or adjusting the cacheWidth/cacheHeight to decode an image to a smaller size reducing the memory usage of the ImageCache. The analysis catches issues like this see Analysis tab.

分配和跟踪

Allocations and tracking

在你感兴趣的时间周期内,监控所有直接与 DevTools 或应用程序交互时涉及到的内存分配。你可以了解分配了多少对象、多少字节,或者跟踪代码中特定类的所有分配位置。此信息可在内存分析页面的 Allocations(分配) 选项下查看,与使用快照相比,它的速度相当快,开销也更小。

Monitoring all allocations involes you directly interacting with DevTools and your application to isolate a short period of time that you are interested in knowing how many objects were allocated, how many bytes were allocated, or tracking all the places in your code where a particular class is allocated. This information is available under the “Allocations” tab of Memory profiler and his a fairly fast with less overhead than using a snapshot.

监控分配和重置累计数据,有助于在点击重置和监视跟踪之间的短时间内分析累计数据(分配的对象数或字节数)。如果你怀疑应用程序发生了内存泄漏或存在与内存分配相关的其他错误,累计数据可以帮助你了解内存分配的速率。此外,跟踪几个特定类的能力,可能会减慢应用程序的运行。VM 在调用类的构造函数(分配)时记录堆栈跟踪。可以在分配内存时找到代码中的确切位置。请参阅 Allocation 选项

Monitoring allocations and resetting of accumulators, helps to analyze the accumulator counts (number of objects or bytes allocated) in a short timeframe between a reset and monitor Track events. The accumulators can be used to understand the rate of memory allocations. If you suspect your application is leaking memory or has other bugs relating to memory allocation. Additionally, the ability to track a few specific classes, too many may slow the running of your application. The VM records the stack trace at the time a class’ constructor (allocation) is called. This can isolate the exact location in your code when/where memory is being allocated. See Allocation tab.

内存剖析图

Memory anatomy

时间序列图用于显示连续时间间隔的 Flutter 内存状态。图表上的每个数据点表示相应时间戳(x 轴)下堆的测量值(y 轴),例如,使用率、容量、外部内存、垃圾收集和常驻集大小。

A timeseries graph is used to visualize the state of the Flutter memory at successive intervals of time. Each data point on the chart corresponds to the timestamp (x-axis) of measured quantities (y-axis) of the heap, for example, usage, capacity, external, garbage collection, and resident set size.

Screenshot of a memory anatomy page

事件窗格

Events Pane

同一个时间轴上会显示 Dart VM 和 DevTools 事件。这些事件包含快照(手动和自动)、Dart VM 自动 GC、手动 GC 或者监控和累计数据的重置操作。

The event timeline displays Dart VM and DevTools events on a shared timeline. These events can be snapshots (manual and auto), Dart VM GCs, user requested GCs, or monitor and accumulator reset actions.

Screenshot of DevTools events

此图表显示与内存图表时间线相关的 DevTools 事件(如手动 GC、VM GC、快照、监控分配 跟踪重置 累计数据按钮单击)。点击事件时间线中的标记,将显示事件发生时间的悬浮窗。这可能有助于判断时间轴(x 轴)中何时发生内存泄漏。

This chart displays DevTools events (such as manual GC, VM GC, Snapshot, monitor Allocations Track and Reset of accumulators button clicks) in relation to the memory chart timeline. Clicking over the markers in the Event timeline displays a hover card of the time when the event occurred. This may help identify when a memory leak might have occurred in the timeline (x-axis).

Screenshot of the event timeline legend

下面是图例中每个 DevTools 事件的符号及其含义

This legend shows the symbol for each DevTools event and its meaning

<p markdown="1">Snapshot</p> <p markdown="1">Snapshot(手动快照)</p>

User Snapshot

用户主动保存的快照,可以收集所有内存信息并进行分析。

User initiated snapshot—all memory information collected and an analysis performed.

<p markdown="1">Auto-Snapshot</p> <p markdown="1">Auto-Snapshot(自动快照)</p>

Auto Snapshot

当检测到内存比以前的大小增加了 40% 或更多时 DevTools 会自动保存一个快照。可以用于快速检测 Flutter 应用程序中的内存峰值,以供后续分析(与手动快照中收集的信息相同)。

DevTools initiated a snapshot detecting that memory grow by 40% or more from previous size. This is used to quickly detect memory spikes in your Flutter application for later analysis (same information collected in a manual snapshot).

<p markdown="1">Track</p> <p markdown="1">Track(跟踪)</p>

Monitor

收集当前所有处于活动状态的类的状态实例数和所有实例的字节大小。此外,变化值是自上次按下 Reset(重置) 按钮以来累计数据的变化。

Collects current state of all active classes number of instances and byte size of all instances. In addition, the deltas are the change in the accumulators since the last “Reset” button pressed.

<p markdown="1">Reset</p> <p markdown="1">Reset(重置)</p>

Reset

实例和字节的累计数据都重置为零时。

When both the instance and bytes accumulators were reset to zero.

<p markdown="1">User Initiated GC</p> <p markdown="1">User Initiated GC(用户手动 GC)</p>

GC

用户向 VM 请求执行内存 GC(仅向 VM 建议,不一定立刻执行)。

User initiated request to VM to to perform a garbage collection of memory (only a suggestion to the VM).

<p markdown="1">VM GC</p> <p markdown="1">VM GC(VM 自动 GC)</p>

VM GC

VM 自动执行 GC,释放不再使用的空间。更多 Dart 是如何执行垃圾收集的信息,参阅 不要担心 GC

GC (VM garbage collection) has occurred, frees space no longer used. For more information on how Dart performs garbage collection, see Don’t Fear the Garbage Collector.

<p markdown="1">User and Flutter Event</p> <p markdown="1">User and Flutter Event(用户和 Flutter 事件)</p>

在事件窗格中显示为三角形。深色三角形表示「多个 Flutter 或用户事件」。

Displayed as a triangle in the event pane. The dark magenta triangle “Multiple Flutter or User Events”

Aggregate Events

标识在此时间戳接收的多个事件。浅色三角形表示「一次 Flutter 或用户事件」。

identifies more than one event was received at this timestamp. The lighter magenta triangle “One Flutter or User Event”

Single Events

表示在此时间戳处仅收到一个事件。要查看事件,点击三角形将显示悬浮窗,展开浮窗底部,将显示该时间戳的所有事件。

indicates only one event was received at this timestamp. To view the events clicking on the triangle will display a hover card and expanding the events at the bottom of the hovercard will display all events for that timestamp.

事件窗格下方显示的是 内存图表Android 内存图表。只有 Android 应用程序才会展示,它显示了 ADB 应用程序摘要中的 Android ADB 内存信息。

Displayed below the events pane is the memory chart and the Android memory chart. The android-memory chart is specific to an Android app, and it shows Android ADB meminfo from an ADB app summary.

添加自定义事件到时间线

Adding user custom events to the timeline

有时可能很难将 Flutter 应用程序代码中的操作与内存时间线图中绘制的内存数据或者事件关联起来。你可以将自定义的事件发送到 内存分析 时间线中,来了解代码中发生了什么。这会帮助你了解应用程序在 Dart/Flutter 框架中的内存使用情况(堆)。

Sometimes it may be difficult to correlate the actions in your Flutter application code and the collected memory statistics/events charted in the Memory timeline/chart. To help know what’s happening in your code your own events can be injected into the Memory Profile timeline to help to understand how your application’s memory usage is performing within the Dart/Flutter framework (heap).

使用 dart:developer 包的 postEvent 方法发布你的自定义事件。需要注意的是,事件名称的前缀必须为 DevTools.Event_,然后附加事件的名称。例如 DevTools.Event_MyEventName

Posting your own custom event(s) are done using the dart:developer package postEvent method. In particular, the event name must be prefixed with DevTools.Event_ then your event name would be appended e.g., DevTools.Event_MyEventName

使用时你需要在代码中添加下方的导入信息:

To use add the following import to your code:

import 'dart:developer' as developer;

以及将自定义事件发布到内存时间线的方法:

and a method to post custom event(s) to the Memory timeline:

  void devToolsPostEvent(String eventName, Map<String, Object> eventData) {
    developer.postEvent('DevTools.Event_$eventName', eventData);
  }

要从代码中发布事件,可以调用 devToolsPostEvent。例如,在函数 recordLoadedImage 中,可以通过 method(recordLoadedImage)以及 param(URL)参数将 MyImages 事件发布到内存(事件)时间线。

Then to post an event from your code you would call the devToolsPostEvent e.g. In your function recordLoadedImage you could cause the ‘MyImages’ event to be posted to the Memory (event) timeline with the values method and param (the URL).

  Widget recordLoadedImage(ImageChunkEvent imageChunkEvent, String imageUrl) {
 
    // Record the event in the memory event pane.
    devToolsPostEvent('MyFirstApp', { 'method': 'recordLoadedImage', 'param': imageUrl });

    if (imageChunkEvent == null) return null;

    ...

  }

点击事件窗格中的多事件三角形,将显示包含所有事件详细信息的悬浮窗,例如,在 04:36:21 时刻的两个自定义事件,事件名称为「MyFirstApp」,两个字段 method 和 param 显示事件的对应值:

Clicking on the aggregated event triangle in the event pane will dispay a hover card with the details of all events e.g., two custom events at the timestamp 04:36:21 with the event name ‘MyFirstApp’ and the two eventData entries method and param are displayed with their values:

Hover Card Custom Events

滑动事件显示:

Scrolling the events displays:

Custom Events Details

内存概览图

Memory overview chart

采集的内存数据的时间序列图,用于观察随时间变化的 Dart/Flutter 堆和本机内存的状态。

A timeseries graph of collected memory statistics, to visualize the state of the Dart/Flutter heap and Dart/Flutter native memory over time.

图表的 x 轴是事件的时间线(时间序列)。在 y 轴上绘制的数据在收集数据时都有时间戳。换句话说,这会显示每隔 500 毫秒内存的状态(容量、已用内存、外部内存、常驻集大小和 GC)。显示应用程序运行实时的内存状态。

The chart’s x-axis is a timeline of events (timeseries). The data plotted in the y-axis all have a timestamp when the data was collected. In other words, it shows the polled state (capacity, used, external, RSS (resident set size), and GC (garbage collection)) of the memory every 500 ms. This helps give a live appearance on the state of the memory as the application is running.

点击图例按钮会显示采集的测量值和用于显示数据的符号或颜色。

Clicking on the Legend button describes the collected measurements and symbols/colors used to display the data.

Screenshot of a memory anatomy page

Y 轴上 内存大小刻度 会自动调整到当前图表收集的数据范围内。

The Memory Size Scale Y axis scale automatically adjusts to the range of data collected in the current visible chart range.

y 轴上绘制的数据包含:

The quantities plotted on the y-axis are:

Dart/Flutter Heap(Dart/Flutter 堆) 堆中的对象 (Dart/Flutter 对象)。

Dart/Flutter Heap Objects (Dart/Flutter objects) in the heap.

Dart/Flutter Native(Dart/Flutter 原生对象) 不在 Dart/Flutter 堆中,但仍然占用总内存的一部分。该内存中存储了原生对象(例如,文件读取或者图片解码的所占用的内存)。原生对象通过 Dart 嵌入层,从原生操作系统(如 Android、Linux、Windows、iOS)暴露给 Dart VM。嵌入层使用 finalizer 创建一个 Dart 包装类,允许 Dart 代码与这些原生资源通信。 Flutter 有一个用于 Android 和 iOS 的嵌入层。更多信息,参阅 服务端应用 Dart自定义 Flutter 引擎嵌入层

Dart/Flutter Native Memory that is not in the Dart/Flutter heap but is still part of the total memory footprint. Objects in this memory would be native objects (for example, from a memory read from a file, or a decoded image). The native objects are exposed to the Dart VM from the native OS (such as Android, Linux, Windows, iOS) using a Dart embedder. The embedder creates a Dart wrapper with a finalizer, allowing Dart code to communicate with these native resources. Flutter has an embedder for Android and iOS. For more information, see Dart on the Server or Custom Flutter Engine Embedders.

Timeline(时间线) 在特定时间点采集的所有内存统计数据和事件的时间戳(时间戳)。

Timeline The timestamps of all collected memory statistics and events at a particular point in time (timestamp).

Raster Cache(光栅缓存) 合成后执行最终渲染时颤振引擎光栅缓存层或图片的光栅缓存大小。详情参阅 Flutter 架构概览DevTools 性能视图

Raster Cache Size of the Flutter engine’s raster cache layer(s) or picture(s) while performing the final rendering after compositing. See Flutter Architectural Overview and DevTools Performance.

Allocated(已分配内存) 堆当前的容量,通常略大于所有堆对象的总大小。

Allocated Current capacity of the heap is typically slightly larger than total size of all heap objects.

RSS - Resident Set Size(常驻集) 常驻集大小显示进程的内存量。包含加载的共享库中的内存,以及所有堆栈和堆内存,不包含交互的内存。

RSS - Resident Set Size The resident set size displays the amount of memory for a process. It doesn’t include memory that is swapped out. It includes memory from shared libraries that are loaded, as well as all stack and heap memory.

有关更多信息,请参阅 Dart 虚拟机结构

For more information, see Dart VM internals.

悬浮窗

Hover card

点击图表会在 X 轴(时间戳)上显示一条垂直黄线,同时显示带有所收集信息的悬浮窗:

Clicking in a chart will display a vertical yellow line where the click occurred on the X-Axis (Timestamp), a hover card will be displayed with the information collected:

Screenshot of the basic memory chart

Memory Events(内存事件) 事件窗口中记录的内存事件,例如虚拟机自动 GC、用户启动的 GC、用户保存的快照、自动快照、分配监控和重置累加数据。

Memory Events Memory Events recorded in the Event Pane e.g., VM GC, User Initiated GC, User Initiated Snapshot, Auto-Snapshot, Allocation Monitoring and, Reset of Accumulators.

Dart / Flutter Memory(Dart / Flutter 内存) 收集的数据容量、已用数据、外部数据、RSS、光栅缓存(图像/图层)。

Dart / Flutter Memory Collected data Capacity, Used, External, RSS, Raster Cache (pictures/layers).

Flutter and User Events(Flutter 和自定义的事件) 扩展事件,例如 Flutter.ImageSizesForFrame,用户自定义事件。参阅 事件

Flutter and User Events Extension events e.g., Flutter.ImageSizesForFrame, user custom events see Events.

顾名思义,集合事件收集了最接近特定时间戳(tick)的所有事件,并将事件显示到 x 轴最近的位置。

Aggregate events, as the name implies, collects all the events nearest a particular timestamp (tick) and displays the events to the x-axis’ closest tick.

如果此时间戳处收集了多个事件,则会显示一个深色三角形,包含事件集合的列表。集合事件收集了最接近特定时间戳 (tick) 的所有事件,并将事件显示到 x 轴最近的位置。展开将显示每个事件的更多信息:

If more than one event, collected at this timestamp, a dark magenta triangle is displayed with the aggregate list of events. The aggregate vents collects all the events nearest a particular timestamp (tick) and displays the events to the X-Axis closest tick. Expanding the events will display the values for each event: Aggregate Events

如果只收集了一个事件,则会显示一个浅色三角形,并显示单个事件信息:

If only one event is collected, a lighter magenta triangle color is displayed with the single event values: Single Events

如果显示 Android 内存图表,则 Android 部分采集的数据将显示在「Dart / Flutter 内存」和「Flutter 和自定义的事件」之间。例如:

If the Android memory chart is displayed then the Android collect data will displayed between the “Dart / Flutter Memory” and the “Flutter and User Events” e.g.,

Hovercard of Android chart is visible

Android 图表

Android chart

当连接到 Android 应用程序时,DevTools 会从 ADB 连接的应用程序摘要中(每 500 毫秒一次)收集 Android 的 ADB(Android 调试通道)内存信息。这部分内存信息非常有趣。如果你从 ADB 工具中采集此信息,它将是这样的:

When connected to an Android app, DevTools collects Android’s ADB (Android Debug Bridge) meminfo from an ADB app summary (polled every 500 ms). This meminfo section is the most interesting at a high-level. If you were to collect this info from the ADB tool, this is what it would look like:

> adb shell dumpsys meminfo io.flutter.demo.gallery -d

 App Summary
                       Pss(KB)
                       -------
           Java Heap:     5192
         Native Heap:    11992
                Code:     2132
               Stack:       60
            Graphics:    53700
       Private Other:    42800
              System:    84493
 
               TOTAL:   200369       TOTAL SWAP PSS:    82168

此图表是应用程序运行时 Android 内存状态的另一个时间序列图。上述值会被绘制到 y 轴上(Java 堆、原生堆、代码大小、堆栈大小、图形堆栈、系统大小和总大小)。

This chart is another timeseries graph of the state of Android memory as the application is running. The quantities plotted on the y-axis are the above values (Java Heap, Native Heap, Code size, Stack size, Graphics stack, System size and total).

点击时间戳(x 位置)将显示该时间段内收集的所有数据点。

Clicking on a timestamp (x-position) will display all data points collected for that time period.

Screenshot of Android Memory Chart

悬浮窗将显示所有采集到的 Android 内存数据。

The hover card will display the values of all collected Android memory data.

<p markdown="1">Time</p> <p markdown="1">Time(时间戳)</p>

内存数据的采集时刻 - 请参阅以下的说明。

The timestamp for the current data values collected - see descriptions below.

<p markdown="1">Total(总计大小)</p>

已使用的总内存大小,由几类不同的内存组成,所有类别都沿着 y 轴绘制,如下所述。

The total memory in use. Total memory is comprised of several different categories, all of which are plotted along the y-axis. These categories are described below.

<p markdown="1">Other</p> <p markdown="1">Other(其他)</p>

「其他」使用情况对应于 ADB 的「Private Other(其他私有)」部分,这是系统不确定如何分类的内存。注意:这一部分是「其他」和「系统」(共享和系统内存使用- 对应 ADB 的 System(系统)部分)的组合。

Other memory usage corresponds to the ‘Private Other’ field from ADB. This is memory used by the app that the system isn’t sure how to categorize. Note: The Other trace is a combination of Other and System (shared and system memory usage) - corresponds to ‘System’ field from ADB.

<p markdown="1">Code</p> <p markdown="1">Code(代码)</p>

「代码」使用情况对应于 ADB 的「Code(代码)」部分。这是应用程序中静态代码和资源的内存,如 dex 字节码、优化或编译的 dex 代码、.so 库和字体。

Code memory usage corresponds to the ‘Code’ field from ADB. This is memory that your app uses for static code and resources, such as dex byte code, optimized or compiled dex code, .so libraries, and fonts.

<p markdown="1">Native Heap</p> <p markdown="1">Native Heap(本地堆)</p>

「本地堆」使用情况对应于 ADB 中的「Native Heap(本地堆)」部分。这是从 C 或 C++ 代码分配的对象的内存。即使你的应用程序中没有使用 C++,也可以看到本地内存的使用,因为 Android 框架使用本地内存来处理各种任务。比如,用来处理图像资源和其他图形,即使你编写的代码是 Java 或 Kotlin。

Native Heap usage corresponds to the ‘Native Heap’ field from ADB. This is memory from objects allocated from C or C++ code. Even if you’re not using C++ in your app, you might see some native memory used here because the Android framework uses native memory to handle various tasks on your behalf. Some examples of these tasks are handling image assets and other graphics—even though the code you’ve written is in Java or Kotlin.

<p markdown="1">Java Heap</p> <p markdown="1">Java Heap(Java 堆)</p>

「Java 堆」使用情况对应于 ADB 的「Java Heap(Java 堆)」部分。这是 Java 或 Kotlin 代码分配的对象的内存。

Java Heap usage corresponds to the ‘Java Heap’ field from ADB. This is memory from objects allocated from Java or Kotlin code.

<p markdown="1">Stack</p> <p markdown="1">Stack(堆栈)</p>

「堆栈」使用情况对应于 ADB 的「Stack(堆栈)」部分。这是应用程序中本地和 Java 堆栈使用的内存。通常与应用程序正在运行的线程数有关。

Stack usage corresponds to the ‘Stack’ field from ADB. This is memory used by both native and Java stacks in your app. This usually relates to how many threads your app is running.

<p markdown="1">Graphics</p> <p markdown="1">Graphics(图形)</p>

「图形」使用情况对应于 ADB 的「Graphics(图形)」部分。这是用于图形缓冲队列在屏幕上显示像素的内存,包括 GL 曲面、GL 纹理等。注意:这是与 CPU 共享的内存,而不是专用的 GPU 内存。

Graphics usage corresponds to the ‘Graphics’ field from ADB. This is memory used for graphics buffer queues to display pixels on the screen, including GL surfaces, GL textures, etc. Note: This is memory shared with the CPU—not dedicated GPU memory.

内存控制

Memory controls

在内存页面顶部的图表上方,有几个按钮和下拉列表,用于控制内存数据的显示方式。

At the top of the memory page, above the charts, are several buttons and dropdowns that control how memory data is displayed.

Screenshot of a memory controls

<p markdown="1">Pause</p> <p markdown="1">Pause(暂停)</p>

暂停内存概览图表的变化,可以检查已展示的数据,但仍会接收到传入的内存数据。注意,范围选择器会继续向右增长。

Pause the memory overview chart to allow inspecting the currently plotted data. Incoming memory data is still received; notice the Range selector continues to grow to the right.

<p markdown="1">Resume</p> <p markdown="1">Resume(继续)</p>

恢复内存概览图表,使其处于活动状态,显示当前时间和最新内存统计数据。

Resume the memory overview chart so that it is live, displaying the current time and the latest memory statistics.

<p markdown="1">Clear</p> <p markdown="1">Clear(清除)</p>

清除内存监控中收集的所有数据。

Clear all collected data from the memory profiler.

<p markdown="1">Display</p> <p markdown="1">Display(显示范围)</p>

x 轴的显示区间。例如,将此下拉列表设置为「显示 5 分钟」,则显示最近 5 分钟的内存数据。

The duration of the x-axis. For example, if this dropdown is set to “Display 5 minutes”, memory data from the last 5 minutes will be displayed.

<p markdown="1">- Display 1 Minute</p> <p markdown="1">- Display 1 Minute(显示最近 1 分钟)</p>
<p markdown="1">- Display 5 Minutes</p> <p markdown="1">- Display 5 Minute(显示最近 5 分钟)</p>
<p markdown="1">- Display 10 Minutes</p> <p markdown="1">- Display 10 Minute(显示最近 10 分钟)</p>
<p markdown="1">- Display All Minutes (slider disabled)</p> <p markdown="1">- Display All Minutes (slider disabled)(显示所有时间 - 禁用滑动)</p>
<p markdown="1">Source</p> <p markdown="1">Source(数据来源)</p>

数据来源可以是「Live Feed」(从连接的 Flutter 应用程序中获取实时数据),也可以是通过点击「导出」创建的本地数据文件。

Source can be either “Live Feed”, which pulls data from the connected Flutter app, or one of the available offline data files, which are created by clicking “Export”.

<p markdown="1">Android Memory</p> <p markdown="1">Android Memory(安卓内存)</p>

显示或隐藏 Android 内存图表。

Displays or hides the Android Memory Chart.

<p markdown="1">GC</p> <p markdown="1">GC(垃圾回收)</p>

启动垃圾回收 - 压缩堆空间。

Initiates a garbage collection - compaction of the heap.

<p markdown="1">Export</p> <p markdown="1">Export(导出)</p>

为事件时间线、内存概览图和 Android 概览图保存已收集的数据。保存的文件显示在「Source(数据来源)」下拉列表中。选择文件将加载数据。

Saves collected data for Event Timeline, Memory Overview Chart and Android Overview Chart. Files saved are displayed under the Source dropdown. Selecting a file loads the offline data.

内存相关操作

Memory actions

内存图表(事件时间线、内存概览和 Android 概览图表)下方有两个 tab, 分别用于分析和收集 DevTools 所连接到的应用程序的内存使用情况。

Below the memory charts (Event Timeline, Memory Overview and Android Overview charts) are interactive actions used to collect and analyze information about memory usage while using the application DevTools is connected to there are two tabs:

Two Tabs Memory Actions

Analysis 选项

Analysis tab

分析选项 会收集由用户手动保存和 DevTools 检测到内存峰值时自动保存的内存快照。每个快照都会被分析并生成分析结果。

The Analysis tab collects memory snapshots both user initiated and auto-collected by DevTools, when DevTools detects memory spikes. Each snapshot is analyzed and an analysis is created too.

Analysis 选项下的操作

Analysis actions

analysis 选项下的操作包括:

The actions available for Analysis are:

Screenshot of a memory actions

Snapshot(快照) 点击 Snapshot 按钮向 Dart VM 发出请求,以保存内存当前的状态。内存对象可以按属性排序,如类名、大小、分配的实例(参见 快照类)。

Snapshot Clicking the Snapshot button makes a request to the Dart VM to collect the current state of memory. The memory objects can be sorted by attributes such as class name, size, allocated instances (see Snapshot classes).

Treemap(树形图) 如果 Treemap 开关打开,快照将以树形图的形式在高级视图中显示当前活动的对象、最后一个快照和内存。(详情待定)

Treemap If the Treemap switch is on the snapshot displays currently active memory objects, the last snapshot, memory in a high-level view as a tree map. (TBD details).

Group By(分组方式) 下拉列表选择数据的分组方式,可以按实例或按类名分组。

Group By Dropdown to select how data is grouped, which can either be by instance or by class name.

Collapse All(折叠全部) 折叠树表中的所有节点。

Collapse All Collapse all nodes in the tree table.

Expand All(展开全部) 展开树表中的所有节点。

Expand All Expand all nodes in the tree table

分析和快照视图

Analysis and Snapshots view

所有分析和快照都显示在树表视图中:

All Analyses and Snapshots are displayed in a Table Tree View:

Two Tabs Memory Actions

快照按库分组,在库中按类分组,每个类将显示该类的已知实例列表。

The snapshots are grouped by library and within library by class and each class will display the list of known instances for that class.

快照是特定时间点上所有内存对象的完整视图。在树中导航到类及其实例(调用构造函数创建的实例)。如果实例存在,扩展类将显示所有活动实例(对象)。点击一个类的实例,将在树表的右侧显示内存的检查信息。

A snapshot is a complete view of all memory objects at a particular point in time. Navigating, in the tree, to a class and it’s instances (if the constructor was called to create an instance). If instances exists expanding the class will display all live instances (objects). Clicking on an instance, of a class, will bring up the memory inspector to the right-side of the table tree.

Two Tabs Memory Actions

快照

Snapshots

The Snapshot button

点击 Snapshot(快照) 按钮显示堆的所有活动的类及其实例的当前状态。

Clicking the Snapshot button shows the current state of the heap with regard to all active classes and their instances.

Screenshot of the Snapshot classes

此窗口显示堆中分配的类、类的所有实例并且可以检查某个特定的实例的信息。

This pane shows classes allocated in the heap, all instances for a class, and the ability to inspect a particular instance.

此外,当 DevTools 检测到所用内存出现峰值(内存增长 > 40%)时,会自动生成快照。

In addition, a snapshot can automatically occur when DevTools notices a spike in memory used (growth of > 40%).

每个快照(手动或自动)都将生成快照的分析。例如,可能发生的组映像问题。将来,其他可能引起内存问题的常见 Flutter 编码问题(例如字体、文件、JSON 等)也会加入到分析中。

Every snapshot, manual or automatic, will generate an analysis of the snapshot e.g., groups image problems that might have occurred. In the future, other common Flutter coding issues e.g., Fonts, Files, JSON, etc. that could cause memory problems will be flagged.

<p markdown="1">Tree View of Memory</p> <p markdown="1">内存树视图</p>

树表视图显示关键的内存事件(用户请求的快照、自动快照、快照分析、内存分配监控)。

The tree table view displays outstanding memory events (user requested snapshots, automatic snapshots, snapshot analyses, memory allocation monitoring).

<p markdown="1">Memory Inspector</p> <p markdown="1">内存检查器</p>

根据树状图中当前选定的行显示其分析、快照或监视的内容。

Display either the contents of an analysis, snapshot or monitoring based on the currently selected row in the tree view.

Snapshots have major tree nodes: 快照具有主要的树节点:

<p markdown="1">External</p> <p markdown="1">External(外部内存)</p>

不在 Dart 堆中但仍然占用总内存一部分的内存。外部内存中的对象可以是原生对象(例如,文件读取或者图片解码的所占用的内存)。原生对象通过 Dart 嵌入层,从原生操作系统(如 Android、Linux、Windows、iOS)暴露给 Dart VM。嵌入层使用 finalizer 创建一个 Dart 包装类,允许 Dart 代码与这些原生资源通信。 Flutter 有一个用于 Android 和 iOS 的嵌入层。更多信息,参阅 服务端应用 Dart自定义 Flutter 引擎嵌入层

Memory that is not in the Dart heap but is still part of the total memory footprint. Objects in external memory would be native objects (for example, from a memory read from a file, or a decoded image). The native objects are exposed to the Dart VM from the native OS (such as Android, Linux, Windows, iOS) using a Dart embedder. The embedder creates a Dart wrapper with a finalizer, allowing Dart code to communicate with these native resources. Flutter has an embedder for Android and iOS. For more information, see Dart on the Server or Custom Flutter Engine Embedders.

<p markdown="1">Filtered</p> <p markdown="1">Filtered(筛选)</p>

筛选项中包含了筛选过的包。

Filter are the packages being filtered.

<p markdown="1">Packages</p> <p markdown="1">Packages(包)</p>

应用程序使用的用户包和 Src — 空的 Dart 包。

User packages used by the application and Src - the empty Dart package.

上述每个节点下都是类节点,是分配给该类的对象的集合。点击类名将显示该类的所有实例。单击某个实例将显示其检查信息(字段和值)。

Under each of the above nodes are class nodes, an aggregate of the objects allocated to this class. Clicking a class name displays a list of class instances. and under each class are all the instances of a class. Clicking on an instance will inspect the contents of that instances (fields and values).

检查快照中的类实例

Inspecting a class instance in a snapshot

展开类将显示该类的活动的实例。点击某个特定实例将显示该实例字段的类型和值。

Expanding a class displays the active instances for that class. Clicking on an particular instance displays the type and value of the fields for that instance.

Screenshot of the inspecting an instance

快照分析

Analysis of a snapshot

每个快照都会在分析节点下创建相应的分析内容(分析的时间对应快照的生成时间)。

Every snapshot creates a corresponding Analyzed entry under the Analysis node (the Analyzed date/time corresponds to the matching Snapshot date/time).

Screenshot of a Snapshot Analysis

目前,分析查找图片的常见问题。例如,加载大文件而不是缩放的缩略图,没使用 ListBuilder 管理列表中的图片等。

Currently, Analysis looks for common problems with images e.g., loading large files instead of scaled thumbnails, not using a ListBuilder to manage images in a list, etc.

该分析从快照中提取所有与图片相关的类和实例,并将数据组织在一个位置。这样我们不必搜索和了解哪些类与图片相关,并检查其实例。

The Analysis pulls all Image related classes and instances from a snapshot and organizes the data in one place instead of having to search for the all the classes and inspect the instances to understand what are just image related classes.

在上图的分析中,原始图片位于外部内存的 _Int32List(或较新手机的 _Int64List)部分,根据实例大小分类到 Buckets 中。可以看出,图片大小为 10K-50K 的有 11 张,10M-50M 有 1 张,1M-10M 有 7 张,大于 50M 的有 4 张。这个应用程序中共有超过 500M 的图片在手机上渲染为小图。

In the above Analysis the raw images are located in the Externals portion of memory _Int32List (or _Int64List for newer phones) organizes the instances sizes into buckets. Eleven images are 10K-50K, one image is 10M-50M, seven images are 1M-10M and four images are greater than 50M. For a grand total of over 500M of this app constitute images rendered as small images on a phone.

Allocation 选项

Allocation tab

Allocation 选项可以监控所有类的实例,报告分配的对象数和所有对象消耗的字节数。数字以绝对总数和累计总数显示。一开始,累计数据(对象数量和字节大小)等于第一次发起监控请求时的初始总数。可以随时将累计数据重置为零,这样下次监控请求将返回自上次重置以来的累计值。

The Allocation tab allows monitoring the instances of all classes, reporting the number of objects allocated and number of bytes consumed by all objects. The numbers are displayed in absolute totals as well as accumulated totals. Initially, the accumulated values (number of objects and size in bytes) are equal to the initial totals at the time of the first monitor request. The accumulators can be reset, to zero, at any time such that the next monitor request will return the accumlated values since the last reset.

此外,一小组类可以跟踪类的每个实例的内存分配。调用构造函数时,监控器可以捕获到对应的堆栈。但跟踪这些分配性能代价很大(缓慢的),因此不要频繁使用跟踪。

Additionally, a small set of classes can track the allocation of each instance of a class. The tracking captures a stack trace when the constructor was called. The overhead to track these allocations are expensive (slow) therefore tracking should be used sparingly.

Allocation actions

Screenshot of a memory actions

<p markdown="1">Track</p> <p markdown="1">Track(跟踪)</p>

记录和监控所有实例的数量和大小(以字节为单位)。点击「Track(跟踪)」按钮,实例分配数据会显示在下方的表格中。对于表中的每个实例,「Delta(增加量)」列表示自上次重置数据以来内存的分配大小。

Records and monitors the number of instances and size of all instances in bytes. Clicking the “Track” button, a table will populate with instance allocation data. For each instance in the allocation table, The “Delta” column reflects the number of memory allocations since the last reset.

<p markdown="1">Reset</p> <p markdown="1">Reset(重置)</p>

重置表中每个实例的累计数据(增加量列)。下次按下「Monitor(监控)」按钮时,增加量列将显示自上次重置以来新分配的实例和所占空间。

Resets the accumulator counts (Delta columns) for each instance in the allocation table. The next time the “Monitor” button is pressed, the “Delta” columns displays the populate with the new instances and sizes since the last reset.

<p markdown="1">Search</p> <p markdown="1">Search(搜索)</p>

搜索功能会存在实例分配数据时可用。输入或从下拉列表中选择名称将跳转到表中对应的类。

The search field is enabled when the instance allocation data exists. Typing, or selecting a name from the dropdown, will navigate to that class name in the table.

<p markdown="1">Filter</p> <p markdown="1">Filter(筛选)</p>

弹出一个包含所有已展示的库和类名的对话框(已选中)。

Display a dialog box of libraries and class names to display (checked on).

分配视图

Allocation view

已连接的应用程序中所有可用的类的分配信息会显示表中:

Allocations are displayed in a table view of each class available to the connected application:

Two Tabs Memory Actions

每行显示类名、实例数和对应分配的增加(自上次重置以来的累计数据)的字节数。

Each row displays the class name the number of instances and bytes allocated with deltas (accumulators since last reset).

<p markdown="1">Track with Stack Trace</p> <p markdown="1">Track with Stack Trace(堆栈跟踪)</p>

如果启用,则在调用类构造函数创建实例时记录堆栈。

If enabled records the stack trace when the instance is created, class constructor called.

<p markdown="1">Class Name</p> <p markdown="1">Class Name(类名)</p>

监控的类名

Class allocations monitored.

<p markdown="1">Total Instances</p> <p markdown="1">Total Instances(实例总计数量)</p>

类活动实例总数。

Total number of active instances for the class.

<p markdown="1">Delta Instances</p> <p markdown="1">Delta Instances(实例的增加数量)</p>

由重置按钮控制的实例累计数据。当按下「Reset(重置)」时,累计数据重置为零,然后每次按下「Track(跟踪)」按钮时,当前总计和增加数量都会更新。

An accumulator of change in instance count controlled by the Reset button. When Reset is pressed the accumulators rest to zero then each time the Track button is pressed the current totals and deltas are updated.

<p markdown="1">Total Bytes</p> <p markdown="1">Total Bytes(总字节数)</p>

为类的所有实例分配的总字节数。

Total number of bytes allocated of all instances for the class.

<p markdown="1">Delta Bytes</p> <p markdown="1">Delta Bytes(增加的字节数)</p>

由重置按钮控制的实例字节变化累计数据。当按下「Reset(重置)」时,累计数据重置为零,然后每次按下「Track(跟踪)」按钮时,当前总计和增加数量都会更新。

An accumulator of change in instance bytes created controlled by the Reset button. When Reset is pressed the accumulators rest to zero then each time the Track button is pressed the current totals and deltas are updated.

<p markdown="1">Timestamp of Last Track</p> <p markdown="1">Timestamp of Last Track(上一次启用跟踪的时间)</p>

按下「Track(跟踪)」按钮的时间。

The time when the Track button was pressed.

<p markdown="1">Change Bubble</p> <p markdown="1">Change Bubble(更改气泡)</p>

小气泡,表示已经在表中记录并更新的更改。

Small bubble to indicate the changes collected have been collected and updated in the table.

更多信息,请参阅 分配跟踪

For more information see Allocation Tracking.

管理堆中的对象和统计数据(分配监控)

Managing the objects and statistics in the heap (Monitor Allocations)

The Monitor Allocations button

点击「Track(跟踪)」按钮可以监控类分配的实例总数和字节总数。此外,为实例的分配维护两个累计数据(总计数量和增加数量),用户可以通过操作(按下重置按钮)重置为零。该机制对于查找内存泄漏非常有用。

Clicking the allocation Track button monitors the total number of instances and total number of bytes allocated for a class. In addition, two accumulators are maintained for instances and bytes allocated these accumulators can be reset, to zero, by user action (pressing the Reset Accumulators button). The mechanism is useful to find memory leaks.

Reset Accumulators button

按下 Reset(重置) 按钮时,所有类别的累计数据都会重置为零。当发生重置时,事件时间线上会出现「监控重置」事件。再次点击 Reset(重置) 按钮将两个累计数据重置为零。

When the Reset button is pressed, the accumulators for all classes resets to zero. When reset is occurs a “monitor reset” event to the Event Timeline. Clicking the Reset button again resets both accumulators to zero.

<p markdown="1">Classes</p> <p markdown="1">Classes(类)</p>

堆中的活动的类。

Active classes in the heap.

<p markdown="1">Instances column</p> <p markdown="1">Instances column(实例列)</p>

堆中所有类的活动对象(实例)总数

Total active objects (instances) for all classes in the heap

<p markdown="1">Delta column</p> <p markdown="1">Delta column(数量增加列)</p>

自上次按下「Reset(重置)」后所有实例的累计数量。点击重置按钮重置类实例的累计数量。这对于查找内存泄漏非常有用。

Accumulator counts of all instances since last “Reset” was pressed. Clicking the Reset button initializes the accumulated (Delta) instances of a class. This is useful for finding memory leaks.

<p markdown="1">Bytes column</p> <p markdown="1">Bytes column(字节列)</p>

堆中类的所有实例的总字节数。

Total bytes consumed for all instances of a class in the heap.

<p markdown="1">Delta column</p> <p markdown="1">Delta column(字节增加列)</p>

自上次按下「Reset(重置)」后所有实例的累计字节数。点击重置按钮重置类实例的累计字节数。这对于查找内存泄漏非常有用。

Accumulator counts bytes allocated since last “Reset” was pressed. Clicking the Reset button initializes the accumulated (Delta) bytes for all instances of a class. This is useful for finding memory leaks.

分配跟踪

Allocation tracking

除了跟踪一个类的所有实例的对象数和字节数外,还可以在调用类的构造函数时记录堆栈,来帮助缩小分配可能出错的范围。为此,请选中类的跟踪复选框,例如:

In addition to tracking the number of objects and bytes consumed for all instances of a class, a stack trace can be recorded when a class’s constructor is called to help narrow where allocations might be astray. To do this enable the Track checkbox for a class e.g.,

Enable Stack Trace Tracking

在你与应用程序交互,想要查看实例分配时,再次按下「Track(跟踪)」按钮。这将更新正在跟踪的实例的数量,例如下图中的 118。展开跟踪的实例将显示所有实例以及创建每个实例的时间,例如:

Interact with your application then when you want to view the instances allocation press the “Track” button again. This will update the count for the instances being tracked e.g., 118 in the below figure. Expanding the instances tracked will display all the instances and timestamp when each instance was created e.g.,

Class Tracking

选择实例将显示调用类的构造函数(已分配)时的堆栈,例如:

Selecting an instance will display the call stack at the time the class’s constructor (allocated) was called e.g.,

Call Stack

筛选、搜索和自动补全

Filtering, Searching and Auto-Complete

「Analysis(分析)」和「Allocations(分配)」选项都支持搜索和筛选。输入要查找的类的名称,例如,ObectWithUniqueId 将返回与与输入字符匹配的列表。列表中的第一项高亮显示。

Both the Analysis and Allocations tabs support searching and filtering Begin typing in the name of the class you’d like to find e.g., ObectWithUniqueId will return a list that matches the characters typed so far. The first item in the list is highlighted.

当自动补全可见时,按下按键:

Pressing a keystroke when auto-complete is visible:

ENTER

选择高亮显示的行(GlobalObjectKey)跳转到当前树表视图(快照)或表(分配)中该类名对应的行。

Selects the highlighted line (GlobalObjectKey) and navigates to the row with that class name in the active tree table (Snapshot) or table (Allocations).

<p markdown="1">UP/DOWN arrows</p> <p markdown="1">上/下箭头</p>

在可能匹配的列表中跳转,突出显示列表中的下一项。

Rotates through the list of possible matches highlighting the next item in the list.

ESCAPE

清除并取消所有搜索。

Clears and cancels all searching.

Searching

输入更多字符来缩小类名范围,例如键入 Obje 显示:

Typing more characters would narrow down the possible class names e.g., typing Obje displays:

Narrower Search

最后,输入 ObjectW 显示精确匹配:

Finally, typing ObjectW displays the exact match:

Narrowed Search

筛选

Filtering

筛选用于将库和类从主列表(表)移动到筛选组,帮助减少在分析内存时不太重要的可见类的数量。

Filtering is used to move libraries and classes from the main list (tables) to a Filter group to help reduce the number of classes visible that are less important while profiling memory.

Filtering

<p markdown="1">Hide Private Classes</p> <p markdown="1">Hide Private Classes(隐藏私有类)</p>

前缀带有下划线类的类。

Class names prefix with an underscore.

<p markdown="1">Hide Classes with No Instances</p> <p markdown="1">Hide Classes with No Instances(隐藏没有实例的类)</p>

从未构造的类将被过滤。

Classes never constructed are filtered.

<p markdown="1">Hide Libraries with No Instances</p> <p markdown="1">Hide Libraries with No Instances(隐藏没有实例的库)</p>

库中所有类都从未被实例化过,将会被隐藏

All classes in a library never constructed the library is filtered.

<p markdown="1">Hide Libraries or Packages</p> <p markdown="1">Hide Libraries or Packages(隐藏库或包)</p>

显示应用程序中使用的所有库的列表。默认情况下启用,系统相关库将被过滤掉(例如:dart:、package:flutter)。如果你对 Dart 核心库和类或 Flutter 框架感兴趣,可以关闭自动过滤掉的库。

List of all libraries used in your application are displayed. By default the libraries enabled above are filtered out (dart:, package:flutter, etc.). The libraries automatically filtered can be enabled if you are interested in Dart core libraries and classes or the Flutter framework.

设置

Setting

内存分析有一个特定的设置对话框:

The Memory profiler has a specific settings dialog:

Settings

<p markdown="1">Collect Android Memory Statistics using ADB</p> <p markdown="1">Collect Android Memory Statistics using ADB(使用 ADB 收集 Android 内存统计信息)</p>

默认情况下,如果 DevTools 通过 Android 设备 / 模拟器连接到应用程序,则不会收集 Android 内存统计信息。使用 ADB 收集开销很大,并且可能会隐藏应用程序中的性能问题。

By default if DevTools is connected to your application via an Android device/emulator then Android memory statistics are not collected. Collecting with ADB can be expensive and may hide performance issues in your app.

<p markdown="1">Display Data in Units (B, K, MB, GB)</p> <p markdown="1">Display Data in Units (B, K, MB, GB)(以单位(B、K、MB、GB)显示数据)</p>

默认情况下,悬浮窗中显示的数据使用单位而不是原始值。关闭此选项将显示原始数字,例如 125M 将显示为 125235712。

By default data displayed in the hover card are scaled using units instead of raw values. Turning off this will display the raw numbers e.g., 125M would display as 125,235,712.

<p markdown="1">Enable advanced memory settings</p> <p markdown="1">Enable advanced memory settings(启用高级内存设置)</p>

如果启用,将显示 GC 按钮,请求虚拟机(手动)垃圾收集内存。此手动 GC 只是对虚拟机的请求。虚拟机可能不压缩、部分压缩或完全压缩堆。

If enabled, the GC button is displayed to ask the VM to garbage collect memory (manually). This manual GC is only a request to the VM. The VM may decide to do no compaction, some compaction or complete compaction of the heap.

内存问题案例学习

Memory problem case study

使用大量网络图片导致内存泄漏的学习,以及有关使用 DevTools 内存分析器、检测内存问题和修复问题的分步说明,请参阅 案例研究

Memory leak study using large network images was added with step-by-step instructions on using DevTools Memory profiler, detecting the memory problem and fixing the problem, see case study.

虚拟机术语表

Glossary of VM terms

以下是一些计算机科学概念,它们将帮助你更好地理解应用程序如何使用内存。

Here are some computer science concepts that will help you better understand how your application uses memory.

<p markdown="1">Garbage collection (GC)</p> <p markdown="1">Garbage collection (垃圾回收)</p>

GC 是搜索堆以定位和回收应用程序不再使用的内存区域的过程。这个过程允许重新使用内存,将应用程序由于内存不足导致的崩溃风险降至最低。 GC 由 Dart VM 自动执行。在 DevTools 中,你可以通过点击 GC 按钮按需执行垃圾回收。

GC is the process of searching the heap to locate, and reclaim, regions of “dead” memory—memory that is no longer being used by an application. This process allows the memory to be re-used and minimizes the risk of an application running out of memory, causing it to crash. Garbage collection is performed automatically by the Dart VM. In DevTools, you can perform garbage collection on demand by clicking the GC button.

<p markdown="1">Heap</p> <p markdown="1">Heap(堆)</p>

动态分配的 Dart 对象存在于称为堆的内存部分中。当没有任何对象指向它,或者当应用程序退出时。在堆中分配的对象将被回收(符合 GC 的条件)。当没有任何东西指向某个对象时,它不处于存活状态。当一个对象被另一个对象指向时,它是存活的。

Dart objects that are dynamically allocated live in a portion of memory called the heap. An object allocated from the heap is freed (eligible for garbage collection) when nothing points to it, or when the application terminates. When nothing points to an object, it is considered to be dead. When an object is pointed to by another object, it is live.

Isolates

Dart 通过 Isolates 的方式支持并发,你可以认为这样的过程没有开销。每个 Isolates 都有自己的内存和代码,不受任何其他 Isolates 的影响。更多信息,请参见 事件循环和 Dart

Dart supports concurrent execution by way of isolates, which you can think of processes without the overhead. Each isolate has its own memory and code that can’t be affected by any other isolate. For more information, see The Event Loop and Dart.

<p markdown="1">Memory leak</p> <p markdown="1">Memory leak(内存泄漏)</p>

当一个对象处于存活状态(意味着有其它对象指向它),但它没有被使用时(因此它不应该有来自其他任何对象的引用),就会发生内存泄漏。这样的对象不能被垃圾收集,因此它会占用堆中的空间并导致内存碎片。内存泄漏会给虚拟机带来不必要的压力,并且可能很难调试。

A memory leak occurs when an object is live (meaning that another object points to it), but it is not being used (so it shouldn’t have any references from other objects). Such an object can’t be garbage collected, so it takes up space in the heap and contributes to memory fragmentation. Memory leaks put unnecessary pressure on the VM and can be difficult to debug.

<p markdown="1">Virtual machine (VM)</p> <p markdown="1">Virtual machine (虚拟机)</p>

Dart 虚拟机是一种直接执行 Dart 代码的软件。

The Dart virtual machine is a piece of software that directly executes Dart code.

使用性能视图 (Performance view)

目录

它是什么?

What is it?

性能视图提供了应用活动的时间线以及性能信息。它由三个部分组成,且每个部分的粒度都更加细。

The performance view offers timing and performance information for activity in your application. It consists of three parts, each increasing in granularity.

Flutter frames chart

This chart contains Flutter frame information for your application. Each bar set in the chart represents a single Flutter frame. The bars are color-coded to highlight the different portions of work that occur when rendering a Flutter frame: work from the UI thread and work from the raster thread (previously known as the GPU thread).

Screenshot from a performance snapshot

Selecting a bar from this chart centers the flame chart below on the timeline events corresponding to the selected Flutter frame. The events are highlighted with blue brackets.

Screenshot from a timeline recording

UI

UI 线程执行 Dart VM 中的 Dart 代码。它包括你的应用程序和 Flutter 框架的所有代码。当你创建或打开一个页面, UI 线程会创建一个图层树和一个轻量级的与设备无关的绘制指令集,并把图层树交给设备的 raster(栅格)线程进行渲染。 不要阻塞这个线程。

The UI thread executes Dart code in the Dart VM. This includes code from your application as well as the Flutter framework. When your app creates and displays a scene, the UI thread creates a layer tree, a lightweight object containing device-agnostic painting commands, and sends the layer tree to the raster thread to be rendered on the device. Do not block this thread.

栅格线程

Raster

栅格化线程(也就是我们之前知道的 GPU 线程)执行 Flutter 引擎中图形相关的代码。这个线程通过与 GPU (图形处理单元) 通信,获取图形树并显示它。你不能直接访问 Raster 线程或它的数据,但如果这个线程较慢,那它肯定是由你的 Dart 代码引起的。图形化库 Skia 运行在这个线程上,有时候也称它为光栅线程。

The raster thread (previously known as the GPU thread) executes graphics code from the Flutter Engine. This thread takes the layer tree and displays it by talking to the GPU (graphic processing unit). You cannot directly access the raster thread or its data, but if this thread is slow, it’s a result of something you’ve done in the Dart code. Skia, the graphics library, runs on this thread.

有时候一个页面的图形层树比较容易构建但 raster 线程的渲染却比较昂贵。在这种情形下,你需要找出导致渲染变慢的代码。为 GPU 设定特定多种类型的 workload 是相当困难的。在一些特定的情形下,多个对象的透明度重叠、剪切或阴影,有可能会导致不必要的 saveLayer() 的调用。

Sometimes a scene results in a layer tree that is easy to construct, but expensive to render on the raster thread. In this case, you need to figure out what your code is doing that is causing rendering code to be slow. Specific kinds of workloads are more difficult for the GPU. They might involve unnecessary calls to saveLayer(), intersecting opacities with multiple objects, and clips or shadows in specific situations.

更多详细信息,请查看文档 定位 GPU 图表中的问题

For more information on profiling, see Identifying problems in the GPU graph.

丢帧 (Jank)

Jank

帧渲染图表使用红色图层显示帧延时。如果一帧的渲染时间超过 16ms,则会被认为此帧是延时的,为了达到帧渲染频率到 60 FPS (每秒帧数),每一帧的渲染时间必须等于或少于 16 ms。如果没有达到这个目标,你会发现 UI 不流畅或丢帧。

The frame rendering chart shows jank with a red overlay. A frame is considered to be janky if it takes more than ~16 ms to complete (for 60 FPS devices). To achieve a frame rendering rate of 60 FPS (frames per second), each frame must render in ~16 ms or less. When this target is missed, you may experience UI jank or dropped frames.

更多关于性能分析信息,请查看文档:Flutter 性能分析

For more information on how to analyze your app’s performance, see Flutter performance profiling.

时间线事件表

Timeline events chart

The timeline events chart shows all event tracing from your application. The Flutter framework emits timeline events as it works to build frames, draw scenes, and track other activity such as HTTP traffic. These events show up here in the Timeline. You can also send your own Timeline events via the dart:developer Timeline and TimelineTask APIs.

Screenshot of timeline events for a frame

The flame chart supports zooming and panning:

火焰图表支持缩放和平移。上下滚动分别进行放大和缩小。你可以通过单击和拖拽图表或水平滚动的方式来移动它。在下一节的描述中,你会了解在 CPU 分析器中单击一个事件来查看 CPU 信息。

You can click an event to view CPU profiling information in the CPU profiler below, described in the next section.

CPU 分析器

CPU Profiler

单击 Record 开始进行记录 CPU 信息,完成后点击 Stop 停止记录,C PU 分析器会把收集的信息推送到VM并分别在不同的信息窗口进行展示调用树 (Call Tree, Bottom Up, and Flame Chart).

Start recording a CPU profile by clicking Record. When you are done recording, click Stop. At this point, CPU profiling data is pulled from the VM and displayed in the profiler views (Call Tree, Bottom Up, and Flame Chart).

分析粒度

Profile granularity

VM 收集 CPU 样本的默认速率为 1/250μs (即每 250 微秒收集一次数据)。一般情况下,Profile granularity 的默认值为 “medium”。可以通过页面顶部下拉列表进行修改。抽样率低、中、高粒度分别顺序对应 1/50μs、1/250μs 和 1/1000μs。正确设定此值对性能分析非常重要。

The default rate at which the VM collects CPU samples is 1 sample / 250 μs. This is selected by default on the CPU profiler view as “Profile granularity: medium”. This rate can be modified via the selector at the top of the page. The sampling rates for low, medium, and high granularity are 1 / 1000 μs, 1 / 250 μs, and 1 / 50 μs, respectively. It is important to know the trade-offs of modifying this setting.

高粒度 的配置会具有更高效的采样率,因此单元时间内采集的 CPU 信息会更加详细且采集样例更多。因些 VM 会被经常中断以收集样本数据,所以这有可能会影响你的应用程序的运行或导致性能下降。 VM 中 CPU 样例数据信息的存储空间是受限制的,所以也会导致 VM 的 CPU 示例缓冲区很快地填充满且会产生溢出。相对低采样率,高采样率存储空间会被迅速填满并会出现溢出。一旦空间溢出,就有可能导致采样数据丢失。

A higher granularity profile has a higher sampling rate, and therefore yields a fine-grained CPU profile with more samples. This may also impact performance of your app since the VM is being interrupted more often to collect samples. This also causes the VM’s CPU sample buffer to overflow more quickly. The VM has limited space where it can store CPU sample information. At a higher sampling rate, the space fills up and begins to overflow sooner than it would have if a lower sampling rate was used. This means that you may not have access to CPU samples from the beginning of the recorded profile.

低粒度 的配置具有较低的采样率,因此单元时间内采集的 CPU 信息会比较粗略且采集样例较少。当然,这样也会对你的应用程序性能影响更小。 VM 示例缓冲区填充速度也会较慢,因此你可以采集到相当长一段时间内应用程序的 CPU 样例数据,这也意味着你有更好的机会去查看 CPU 样例数据。

A lower granularity profile has a lower sampling rate, and therefore yields a coarse-grained CPU profile with fewer samples. However, this impacts your app’s performance less. The VM’s sample buffer also fills more slowly, so you can see CPU samples for a longer period of app run time. This means that you have a better chance of viewing CPU samples from the beginning of the recorded profile.

火焰图表

Flame chart

火焰图选项卡主要用于显示一段持续时间内 CPU 的样本信息。图表展示的是自上而下的调用堆栈信息,即上面的堆栈帧调用下面的堆栈帧。每一个堆栈帧的宽度代表 CPU 执行的时长。栈帧消耗 CPU 的时间越长,就越洽有可能是我们进行性能改进的好地方。

This tab of the profiler shows CPU samples for the recorded duration. This chart should be viewed as a top-down stack trace, where the top-most stack frame calls the one below it. The width of each stack frame represents the amount of time it consumed the CPU. Stack frames that consume a lot of CPU time may be a good place to look for possible performance improvements.

Screenshot of a flame chart

调用树 (也叫跟踪树)

Call tree

调用树视图是一种自上而下展示 CPU 中的调用堆栈信息方法。在下图中的表格中可以看出,展开其中的一个方法可以查看它所有的 调用者

The call tree view shows the method trace for the CPU profile. This table is a top-down representation of the profile, meaning that a method can be expanded to show its callees.

总时间Total time

此方法运行的总时间,包括了调用者的执行时间(即调用此方法整个的生命周期时长)。Time the method spent executing its own code as well as the code for its callees.

自执行时间Self time
仅表示执行当前方法把花费的时长。Time the method spent executing only its own code.
方法Method</b>
调用的方法名称。Name of the called method.
源码Source
方法所在的文件路径。File path for the method call site.

Screenshot of a call tree table

自下而上

Bottom up

自下而上 视图也是用于显示方法调用堆栈,但顾名思义,它是一个自下而上的表示方式。这意味着表格中的每个最上方的方法实际上是给定 CPU 样本的调用堆栈中的最后一个方法 (换句话说,这是样本的叶节点)。

The bottom up view shows the method trace for the CPU profile but, as the name suggests, it’s a bottom-up representation of the profile. This means that each top-level method in the table is actually the last method in the call stack for a given CPU sample (in other words, it’s the leaf node for the sample).

在这张表中,可以展开一个方法查看它的所有 调用者

In this table, a method can be expanded to show its callers.

总时间Total time

此方法运行的总时间,包括了调用者的执行时间(即调用此方法整个的生命周期时长)。Time the method spent executing its own code as well as the code for its callee.

自执行时间Self time

在自下而上调用树中对于最顶层的方法(叶堆栈帧),它表示执行自己的代码所需要的时间。对于子节点(调用者),它表示调用者运行被调用者的时间。在下面的这个例子中,调用者 `createRenderObject` 的执行时间等于被调用者 `debugCheckHasDirectionality` 的执行时间。For top-level methods in the bottom-up tree (leaf stack frames in the profile), this is the time the method spent executing only its own code. For sub nodes (the callers in the CPU profile), this is the self time of the callee when being called by the caller. In the following example, the self time of the caller `createRenderObject` is equal to the self time of the callee `debugCheckHasDirectionality` when being called by the caller.

方法Method
调用方法的名称。Name of the called method.
源码Source
方法所在的文件路径。File path for the method call site.

Screenshot of a bottom up table

导入导出

Import and export

DevTools 支持导入和导出时间线快照。单击 export 按钮 (帧渲染图表右上角) 下载当前时间线的快照。要导入时间线快照,可以从任何页面拖放快照到 DevTools。提示 : DevTools 仅支持导入 DevTools 导出的源文件。

DevTools supports importing and exporting performance snapshots. Clicking the export button (upper-right corner above the frame rendering chart) downloads a snapshot of the current data on the performance page. To import a performance snapshot, you can drag and drop the snapshot into DevTools from any page. Note that DevTools only supports importing files that were originally exported from DevTools.

使用 CPU 探测视图

目录

它是什么?

What is it?

CPU 探测视图能够测量并记录你的 Dart 或 Flutter 应用的片段。

The CPU profiler view allows you to record and profile a session from your Dart or Flutter application.

CPU 分析器

CPU Profiler

单击 Record 开始进行记录 CPU 信息,完成后点击 Stop 停止记录,C PU 分析器会把收集的信息推送到VM并分别在不同的信息窗口进行展示调用树 (Call Tree, Bottom Up, and Flame Chart).

Start recording a CPU profile by clicking Record. When you are done recording, click Stop. At this point, CPU profiling data is pulled from the VM and displayed in the profiler views (Call Tree, Bottom Up, and Flame Chart).

分析粒度

Profile granularity

VM 收集 CPU 样本的默认速率为 1/250μs (即每 250 微秒收集一次数据)。一般情况下,Profile granularity 的默认值为 “medium”。可以通过页面顶部下拉列表进行修改。抽样率低、中、高粒度分别顺序对应 1/50μs、1/250μs 和 1/1000μs。正确设定此值对性能分析非常重要。

The default rate at which the VM collects CPU samples is 1 sample / 250 μs. This is selected by default on the CPU profiler view as “Profile granularity: medium”. This rate can be modified via the selector at the top of the page. The sampling rates for low, medium, and high granularity are 1 / 1000 μs, 1 / 250 μs, and 1 / 50 μs, respectively. It is important to know the trade-offs of modifying this setting.

高粒度 的配置会具有更高效的采样率,因此单元时间内采集的 CPU 信息会更加详细且采集样例更多。因些 VM 会被经常中断以收集样本数据,所以这有可能会影响你的应用程序的运行或导致性能下降。 VM 中 CPU 样例数据信息的存储空间是受限制的,所以也会导致 VM 的 CPU 示例缓冲区很快地填充满且会产生溢出。相对低采样率,高采样率存储空间会被迅速填满并会出现溢出。一旦空间溢出,就有可能导致采样数据丢失。

A higher granularity profile has a higher sampling rate, and therefore yields a fine-grained CPU profile with more samples. This may also impact performance of your app since the VM is being interrupted more often to collect samples. This also causes the VM’s CPU sample buffer to overflow more quickly. The VM has limited space where it can store CPU sample information. At a higher sampling rate, the space fills up and begins to overflow sooner than it would have if a lower sampling rate was used. This means that you may not have access to CPU samples from the beginning of the recorded profile.

低粒度 的配置具有较低的采样率,因此单元时间内采集的 CPU 信息会比较粗略且采集样例较少。当然,这样也会对你的应用程序性能影响更小。 VM 示例缓冲区填充速度也会较慢,因此你可以采集到相当长一段时间内应用程序的 CPU 样例数据,这也意味着你有更好的机会去查看 CPU 样例数据。

A lower granularity profile has a lower sampling rate, and therefore yields a coarse-grained CPU profile with fewer samples. However, this impacts your app’s performance less. The VM’s sample buffer also fills more slowly, so you can see CPU samples for a longer period of app run time. This means that you have a better chance of viewing CPU samples from the beginning of the recorded profile.

火焰图表

Flame chart

火焰图选项卡主要用于显示一段持续时间内 CPU 的样本信息。图表展示的是自上而下的调用堆栈信息,即上面的堆栈帧调用下面的堆栈帧。每一个堆栈帧的宽度代表 CPU 执行的时长。栈帧消耗 CPU 的时间越长,就越洽有可能是我们进行性能改进的好地方。

This tab of the profiler shows CPU samples for the recorded duration. This chart should be viewed as a top-down stack trace, where the top-most stack frame calls the one below it. The width of each stack frame represents the amount of time it consumed the CPU. Stack frames that consume a lot of CPU time may be a good place to look for possible performance improvements.

Screenshot of a flame chart

调用树 (也叫跟踪树)

Call tree

调用树视图是一种自上而下展示 CPU 中的调用堆栈信息方法。在下图中的表格中可以看出,展开其中的一个方法可以查看它所有的 调用者

The call tree view shows the method trace for the CPU profile. This table is a top-down representation of the profile, meaning that a method can be expanded to show its callees.

总时间Total time

此方法运行的总时间,包括了调用者的执行时间(即调用此方法整个的生命周期时长)。Time the method spent executing its own code as well as the code for its callees.

自执行时间Self time
仅表示执行当前方法把花费的时长。Time the method spent executing only its own code.
方法Method</b>
调用的方法名称。Name of the called method.
源码Source
方法所在的文件路径。File path for the method call site.

Screenshot of a call tree table

自下而上

Bottom up

自下而上 视图也是用于显示方法调用堆栈,但顾名思义,它是一个自下而上的表示方式。这意味着表格中的每个最上方的方法实际上是给定 CPU 样本的调用堆栈中的最后一个方法 (换句话说,这是样本的叶节点)。

The bottom up view shows the method trace for the CPU profile but, as the name suggests, it’s a bottom-up representation of the profile. This means that each top-level method in the table is actually the last method in the call stack for a given CPU sample (in other words, it’s the leaf node for the sample).

在这张表中,可以展开一个方法查看它的所有 调用者

In this table, a method can be expanded to show its callers.

总时间Total time

此方法运行的总时间,包括了调用者的执行时间(即调用此方法整个的生命周期时长)。Time the method spent executing its own code as well as the code for its callee.

自执行时间Self time

在自下而上调用树中对于最顶层的方法(叶堆栈帧),它表示执行自己的代码所需要的时间。对于子节点(调用者),它表示调用者运行被调用者的时间。在下面的这个例子中,调用者 `createRenderObject` 的执行时间等于被调用者 `debugCheckHasDirectionality` 的执行时间。For top-level methods in the bottom-up tree (leaf stack frames in the profile), this is the time the method spent executing only its own code. For sub nodes (the callers in the CPU profile), this is the self time of the callee when being called by the caller. In the following example, the self time of the caller `createRenderObject` is equal to the self time of the callee `debugCheckHasDirectionality` when being called by the caller.

方法Method
调用方法的名称。Name of the called method.
源码Source
方法所在的文件路径。File path for the method call site.

Screenshot of a bottom up table

使用网络视图 (Network View)

目录

What is it?

The network view allows you to inspect HTTP, HTTPS, and web socket traffic from your Dart or Flutter application.

Screenshot of the network screen

How to use it

Network traffic should be recording by default when you open the Network page. If it is not, click the Record network traffic button in the upper left to begin polling.

Select a network request from the table (left) to view details (right). You can inspect general and timing information about the request, as well as the content of response and request headers and bodies.

Search and filtering

You can use the search and filter controls to find a specific request or filter requests out of the request table.

Screenshot of the network screen

To apply a filter, press the filter button (right of the search bar). You will see a filter dialog pop up:

Screenshot of the network screen

The filter query syntax is described in the dialog. You can filter network requests by the following keys:

Any text that is not paired with an available filter key will be queried against all categories (method, uri, status, type).

Example filter queries:

my-endpoint m:get t:json s:200
https s:404

Other resources

HTTP and HTTPs requests are also surfaced in the Timeline as asynchronous timeline events. Viewing network activity in the timeline can be useful if you want to see how HTTP traffic aligns with other events happening in your app or in the Flutter framework.

使用调试器工具

目录

开始使用

Getting started

开发工具中包含了一个完整的源码级调试器,支持断点、单步调试以及变量检视。

DevTools includes a full source-level debugger, supporting breakpoints, stepping, and variable inspection.

When you open the debugger tab, you should see the source for the main entry-point for your app loaded in the debugger.

In order to browse around more of your application sources, click Libraries (top right) or use the hot key command ⌘ + P / ctrl + P. This will open the libraries window and allow you to search for other source files.

Screenshot of the debugger tab

设置断点

Setting breakpoints

可以点击源码区左边空白(行数展示栏内)来设置断点。单击一次就设置了一个断点,并且也会在 Breakpoints 区域展示出来。再次单击则取消断点。

To set a breakpoint, click the left margin (the line number ruler) in the source area. Clicking once sets a breakpoint, which should also show up in the Breakpoints area on the left. Clicking again removes the breakpoint.

调用栈和变量区

The call stack and variable areas

当应用运行到某个断点时,就会在此处暂停,调试器也会在源码区显示当前暂停的位置。此外,Call stackVariables 区域也会显示暂停时的调用栈以及选中帧的本地变量。在 Call stack 选择其他的帧可以改变变量区的内容。

When your application encounters a breakpoint, it pauses there, and the DevTools debugger shows the paused execution location in the source area. In addition, the Call stack and Variables areas populate with the current call stack for the paused isolate, and the local variables for the selected frame. Selecting other frames in the Call stack area changes the contents of the variables.

Variables 内,可以通过点击对象展开查看其内容来检视独立的对象。指针停在 Variables 区域的对象上时会调用该对象的 toString() 方法并展示结果。

Within the Variables area, you can inspect individual objects by toggling them open to see their fields. Hovering over an object in the Variables area calls toString() for that object and displays the result.

单步调试源码

Stepping through source code

三个单步调试按钮在暂停后会变为可用状态。

When paused, the three stepping buttons become active.

另外,Resume 按钮的作用是恢复应用的正常执行。

In addition, the Resume button continues regular execution of the application.

命令行输出

Console output

运行中应用的命令行输出(stdout 和 stderr)会在命令行中输出,该区域在源代码区下方。Logging view 中也可以看到相应输出。

Console output for the running app (stdout and stderr) is displayed in the console, below the source code area. You can also see the output in the Logging view.

异常跳出

Breaking on exceptions

请在调试器视图顶部切换 Ignore 下拉菜单来适配异常跳出的行为。

To adjust the stop-on-exceptions behavior, toggle the Ignore dropdown at the top of the debugger view.

Break on unhandled exceptions:只在断点被认为应用内代码无法捕获时暂停执行。 Breaking on all exceptions:无论是否被捕获都会暂停执行。

Breaking on unhandled excepts only pauses execution if the breakpoint is considered uncaught by the application code. Breaking on all exceptions causes the debugger to pause whether or not the breakpoint was caught by application code.

已知问题

Known issues

当 Flutter 应用执行热重载时,用户的断点会被清除。

When performing a hot restart for a Flutter application, user breakpoints are cleared.

其他资源

Other resources

访问 Debugging 页面来获取更多关于调试器和性能分析的信息。

For more information on debugging and profiling, see the Debugging page.

使用日志视图 (Logging view)

目录

简介

What is it?

日志视图展示 Dart 运行时和应用框架(比如 Flutter)的事件,以及应用级日志。

The logging view displays events from the Dart runtime, application frameworks (like Flutter), and application-level logging events.

标准日志事件

Standard logging events

默认情况下,日志视图会展示:

By default, the logging view shows:

日志视图的截图

应用日志

Logging from your application

要在代码中输出日志,请查看 添加输出代码的方式调试 Flutter 应用 页面的 日志 部分。

To implement logging in your code, see the Logging section in the Debugging Flutter apps programmatically page.

清理日志

Clearing logs

要清理日志视图的日志记录,请点击 Clear logs(清理日志)按钮。

To clear the log entries in the logging view, click the Clear logs button.

使用应用体积工具

目录

这是什么?

What is it?

应用程序体积工具可让您分析应用的总体积。您可以使用 Analysis 标签 来查看「体积信息」的单个快照,或使用 Diff 标签 比较使用「体积信息」的两个不同快照。

The app size tool allows you to analyze the total size of your app. You can view a single snapshot of “size information” using the Analysis tab, or compare two different snapshots of “size information” using the Diff tab.

什么是“体积信息”?

What is “size information”?

「体积信息」包含 Dart 代码、原生代码和非代码部分(比如应用包,资源和字体)。一个「体积信息」文件包含您应用的所有图片数据。

“Size information” contains size data for Dart code, native code, and non-code elements of your app, like the application package, assets and fonts. A “size information” file contains data for the total picture of your application size.

Dart 体积信息

Dart size information

Dart AOT 编译器会在编译应用程序时对代码进行摇树优化(仅限 profile 或 release 模式 - AOT 编译器不用于 debug 生成,debug 模式是 JIT 编译的)。这意味着,编译器会尝试删除未使用或无法访问的代码,对应用体积进行优化。

The Dart AOT compiler performs tree-shaking on your code when compiling your application (profile or release mode only—the AOT compiler is not used for debug builds, which are JIT compiled). This means that the compiler attempts to optimize your app’s size by removing pieces of code that are unused or unreachable.

当编译器尽全力优化您的代码后,产出的二进制文件会包含依赖、库、类和函数的集合,以及他们的体积(以字节为单位)。这是我们可以在应用体积工具中分析的 Dart 部分的「体积信息」, 有了这些信息,我们便可以进一步优化 Dart 代码,并且跟踪体积问题。

After the compiler optimizes your code as much as it can, the end result can be summarized as the collection of packages, libraries, classes, and functions that exist in the binary output, along with their size in bytes. This is the Dart portion of “size information” we can analyze in the app size tool to further optimize Dart code and track down size issues.

如何使用

How to use it

如果 DevTools 已经连接到了一个正在运行的应用,点击 “App Size” 标签。

If DevTools is already connected to a running application, navigate to the “App Size” tab.

Screenshot of app size tab

如果 DevTools 未连接到应用,您可以从启动 DevTools 后出现的登录页访问该工具(查看安装说明)。

If DevTools is not connected to a running application, you can access the tool from the landing page that appears once you have launched DevTools (see installation instructions).

Screenshot of app size access on landing page

分析标签页

Analysis tab

「分析」标签页允许您检查体积信息的单个快照。您可以看到层次结构的树状图和表格,并且可以使用 “dominator tree” 和 “call graph” 看到代码的属性数据(例如:为什么编译后的应用程序中包含一段代码)。

The analysis tab allows you to inspect a single snapshot of size information. You can view the hierarchical structure of the size data using the treemap and table, and you can view code attribution data (for example, why a piece of code is included in your compiled application) using the dominator tree and call graph.

Screenshot of app size analysis

读取一个体积文件

Loading a size file

当您打开分析标签页时,您可以看到加载一个体积文件的使用说明。拖动一个尺寸文件到弹框中,并点击 “Analyze Size”。

When you open the Analysis tab, you’ll see instructions to load an app size file. Drag and drop an app size file into the dialog, and click “Analyze Size”.

Screenshot of app size analysis loading screen

查看 生成体积文件 可以得到有关生成尺寸文件的信息。

See Generating size files below for information on generating size files.

树状图和表格

Treemap and table

树状图和表格可以查看您的应用体积的结构化数据。

The treemap and table show the hierarchical data for your app’s size.

使用树状图

Using the treemap

树状图是数据结构的可视化表示。在视图中,空间被分解成矩形,其中每个矩形的体积和顺序由一些定量变量决定 (在本例中,体积以字节为单位)。每个矩形的面积与节点在编译后的应用程序中所占的大小成比例关系。在每个矩形(称为 A)的内部,还有更多的矩形存在于数据层次结构的更深层(A 的子级)。

A treemap is a visualization for hierarchical data. The space is broken up into rectangles, where each rectangle is sized and ordered by some quantitative variable (in this case, size in bytes). The area of each rectangle is proportional to the size the node occupies in the compiled application. Inside of each rectangle (call one A), there are additional rectangles that exist one level deeper in the data hierarchy (children of A).

要查看树状图中的单元格的详情,请选择这个单元格。这将重新确定树的根节点,以便选中的单元格作为树状图中新的根节点。

To drill into a cell in the treemap, select the cell. This re-roots the tree so that the selected cell becomes the visual root of the treemap.

如果要后退或向上导航,请使用树映射顶部的面包屑导航。

To navigate back, or up a level, use the breadcrumb navigator at the top of the treemap.

Screenshot of treemap breadcrumb navigator

支配树和调用图

Dominator tree and call graph

这个部分显示了代码的体积属性信息(例如:为什么编译后的应用程序中包含一段代码)。这些数据以支配树和调用图的形式呈现。

This section of the page shows code size attribution data (for example, why a piece of code is included in your compiled application). This data is visible in the form of a dominator tree as well as a call graph.

使用支配树

Using the dominator tree

支配树 是一个树形结构的图表,其子节点可以立刻被支配。如果通往 b 的每条路径都必经节点 a,那么我们可以说:节点 a 支配了节点 b

A dominator tree is a tree where each node’s children are those nodes it immediately dominates. A node a is said to “dominate” a node b if every path to b must go through a.

把它放在应用程序大小分析的上下文中,想象一下 package:a 导入了 package:bpackage:c,并且 package:bpackage:c 都导入了 package:d

To put it in context of app size analysis, imagine package:a imports both package:b and package:c, and both package:b and package:c import package:d.

package:a
|__ package:b
|   |__ package:d
|__ package:c
    |__ package:d

在这个例子中,package:a 支配 package:d,所以这个支配树看起来像是这样:

In this example, package:a dominates package:d, so the dominator tree for this data would look like:

package:a
|__ package:b
|__ package:c
|__ package:d

这些信息对于您而言,可以帮助您理解编译后的应用程序中为何出现某些代码片段。例如,如果您正在分析应用程序的体积,并发现编译后的应用程序中包含意外的包,则可以使用支配树来跟踪包到其根源。

This information is helpful for understanding why certain pieces of code are present in your compiled application. For example, if you are analyzing your app size and find an unexpected package included in your compiled app, you can use the dominator tree to trace the package to its root source.

Screenshot of code size dominator tree

使用调用图

Using the call graph

调用图提供了与支配树相似的信息,帮助您理解编译后的应用程序中为何出现某些代码片段。它并不像支配树一样提供了一对多的代码体积数据节点,而是展示了代码体积数据的节点之间存在的多对多关系。

A call graph provides similar information to the dominator tree in regards to helping you understand why code exists in a compiled application. However, instead of showing the one-to-many dominant relationships between nodes of code size data like the dominator tree, the call graph shows the many-to-many relationships that existing between nodes of code size data.

我们再来看下面这个例子:

Again, using the following example:

package:a
|__ package:b
|   |__ package:d
|__ package:c
    |__ package:d

此数据的调用图会将直接调用者 package:bpackage:cpackage:d 链接到一起,而不是它的「支配者」 package:a

The call graph for this data would link package:d to its direct callers, package:b and package:c, instead of its “dominator”, package:a.

package:a --> package:b -->
                              package:d
package:a --> package:c -->

这些信息对于理解代码片段(包、库、类和函数)之间的细粒度依赖关系非常有用。

This information is useful for understanding the fine-grained dependencies of between pieces of your code (packages, libraries, classes, functions).

Screenshot of code size call graph

我应该使用支配树还是调用图?

Should I use the dominator tree or the call graph?

如果您想理解应用程序中包含一段代码的 根本 原因,请使用支配树。如果您想理解一段代码之间的所有调用路径,请使用调用图。

Use the dominator tree if you want to understand the root cause for why a piece of code is included in your application. Use the call graph if you want to understand all the call paths to and from a piece of code.

支配树是调用图数据的分析或切片,其中节点是通过「支配」而不是父子层次结构连接。在父节点支配子节点的情况下,调用图和支配树中的关系是相同的,但情况并非总是如此。

A dominator tree is an analysis or slice of call graph data, where nodes are connected by “dominance” instead of parent-child hierarchy. In the case where a parent node dominates a child, the relationship in the call graph and the dominator tree would be identical, but this is not always the case.

在调用图完成的情况下(每对节点之间存在一条边),支配树将显示出 root 是图中每个节点的支配者。调用图可以让您更好地理解为什么在应用程序中包含一段代码。

In the scenario where the call graph is complete (an edge exists between every pair of nodes), the dominator tree would show the that root is the dominator for every node in the graph. This is an example where the call graph would give you a better understanding around why a piece of code is included in your application.

差异标签页

Diff tab

diff 标签页让您可以比较体积信息的两个快照。您要比较的两个体积信息文件应该从同一个应用程序的两个不同版本生成,例如,在更改代码之前和之后生成的体积文件。您可以使用树状图和表格可视化两个数据集之间的差异。

The diff tab allows you to compare two snapshots of size information. The two size information files you are comparing should be generated from two different versions of the same app; for example, the size file generated before and after changes to your code. You can visualize the difference between the two data sets using the treemap and table.

Screenshot of app size diff

读取体积文件

Loading size files

当您打开 Diff 标签页时,您将看到加载「旧」和「新」大小文件的使用说明。同样,这些文件需要从同一个应用程序生成。将这些文件拖放到各自的对话框中,然后单击 Analyze Diff

When you open the Diff tab, you’ll see instructions to load “old” and “new” size files. Again, these files need to be generated from the same application. Drag and drop these files into their respective dialogs, and click Analyze Diff.

Screenshot of app size diff loading screen

查看 生成体积文件 可以得到有关生成这些文件的信息。

See Generating size files below for information on generating these files.

树状图和表格

Treemap and table

在差异视图中, 这个树状图和表格只会显示导入的两个文件中的差异数据。

In the diff view, the treemap and tree table show only data that differs between the two imported size files.

关于树状图的问题,可以查看使用树状图

For questions about using the treemap, see Using the treemap above.

生成尺寸文件

Generating size files

要使用应用体积工具,您需要生成一个 flutter 体积分析文件。此文件包含整个应用程序的体积信息(本机代码、Dart 代码、资源和字体等),您可以使用 --analyze size 标志生成它:

To use the app size tool, you’ll need to generate a Flutter size analysis file. This file contains size information for your entire application (native code, Dart code, assets, fonts, etc.), and you can generate it using the --analyze-size flag:

flutter build <your target platform> --analyze-size

这会构建您的应用并输出尺寸的摘要到命令行,同时告诉您在哪里找到体积分析文件。

This builds your application, prints a size summary to the command line, and prints a line telling you where to find the size analysis file.

flutter build apk --analyze-size --target-platform=android-arm64
...
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
app-release.apk (total compressed)                               6 MB
...
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
A summary of your APK analysis can be found at: build/apk-code-size-analysis_01.json

在这个示例中,想要进行更进一步的分析,可以导入 build/apk-code-size-analysis_01.json 文件到体积分析工具。更多信息,可以查看 应用体积尺寸文档

In this example, import the build/apk-code-size-analysis_01.json file into the app size tool to analyze further. For more information, see App Size Documentation.

Flutter SDK overview

目录

The Flutter SDK has the packages and command-line tools that you need to develop Flutter apps across platforms. To get the Flutter SDK, see Install.

What’s in the Flutter SDK

The following is available through the Flutter SDK:

Note: For more information about the Flutter SDK, see its README file.

flutter command-line tool

The flutter CLI tool (flutter/bin/flutter) is how developers (or IDEs on behalf of developers) interact with Flutter.

dart command-line tool

The dart CLI tool is available with the Flutter SDK at flutter/bin/dart.

升级你的 Flutter 版本

目录

无论你使用哪个 Flutter 发布渠道,你都可以使用 flutter 命令来更新 Flutter SDK 和应用所依赖的 packages。

No matter which one of the Flutter release channels you follow, you can use the flutter command to upgrade your Flutter SDK or the packages that your app depends on.

升级 Flutter SDK

Upgrading the Flutter SDK

如果要升级 Flutter SDK 的话,请使用 flutter upgrade 命令:

To update the Flutter SDK use the flutter upgrade command:

$ flutter upgrade

这个命令首先获取你的 Flutter 渠道可用的最新的 Flutter SDK 版本。接着这个命令更新你 app 依赖的每一个 package,到最新的兼容版本。

This command gets the most recent version of the Flutter SDK that’s available on your current Flutter channel.

如果你想使用一个更加新的 Flutter SDK 版本,按照下面的步骤切换到相应的渠道 (channel),接着再运行 flutter upgrade

If you want an even more recent version of the Flutter SDK, switch to a less stable Flutter channel and then run flutter upgrade.

切换 Flutter 发布渠道

Switching Flutter channels

Flutter 有 4个发布渠道,分别是 stable, beta, dev, 和 master。我们推荐使用 stable 渠道除非你需要更加新的版本。

Flutter has four release channels: stable, beta, dev, and master. We recommend using the stable channel unless you need a more recent release.

要查看你当前使用的哪个渠道,使用下面的命令:

To view your current channel, use the following command:

$ flutter channel

要切换到其它渠道,使用 flutter channel <channel-name>。当你切换了渠道以后,使用 flutter upgrade 下载 Flutter SDK 和依赖的 packages。例如:

To change to another channel, use flutter channel <channel-name>. Once you’ve changed your channel, use flutter upgrade to download the Flutter SDK and dependent packages. For example:

$ flutter channel dev
$ flutter upgrade

仅更新 packages

Upgrading packages

如果你修改了 pubspec.yaml 文件,或者想仅更新项目依赖的 packages,而不是同时更新 packages 和 Flutter SDK,可以选择使用下面提到的 flutter pub 命令。

If you’ve modified your pubspec.yaml file, or you want to update only the packages that your app depends upon (instead of both the packages and Flutter itself), then use one of the flutter pub commands.

为了把 pubspec.yaml 文件里列出的所有依赖更新到 最新的兼容版本 ,可以使用使用 upgrade 命令:

To update to the latest compatible versions of all the dependencies listed in the pubspec.yaml file, use the upgrade command:

$ flutter pub upgrade

如果需要自动判断那些过时了的 package 依赖以及获取更新建议,现在你可以使用 outdated 命令。更多相关的信息,请参考 Dart 文档中关于 pub outdated 的说明。

To identify out-of-date package dependencies and get advice on how to update them, use the outdated command. For details, see the Dart pub outdated documentation.

$ flutter pub outdated

获得最新通知

Keeping informed

我们将在 Flutter 通知邮件列表 上发布重大更改的公告。你也可以在 Flutter 开发邮件列表 上提问!除了订阅接收公告外我们很乐意听取您的意见!

We publish breaking change announcements to the Flutter announcements mailing list. You can also ask questions on the Flutter dev mailing list. Aside from subscribing to receive announcements, we’d love to hear from you!

Flutter SDK 版本列表

Flutter 的 Stable channel 是相对稳定的发布版本,查阅这个文档了解更多:Flutter 的构建(发布)渠道 channels

The Stable channel contains the most stable Flutter builds. See Flutter’s channels for details.

Stable channel (Windows)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Beta channel (Windows)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Dev channel (Windows)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Stable channel (macOS)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Beta channel (macOS)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Dev channel (macOS)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Stable channel (Linux)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Beta channel (Linux)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Dev channel (Linux)

请从下列列表中选择:

Select from the following scrollable list:

版本Ref发布日期
Loading...

Master channel

我们并没有对 master channel 的提供打包下载,不过,你可以通过 git clone 我们在 Github 上 repo 的 master 分支来使用。

Installation bundles are not available for master. However, you can get the SDK directly from GitHub repo by cloning the master channel, and then triggering a download of the SDK dependencies:

$ git clone -b master https://github.com/flutter/flutter.git
$ ./flutter/bin/flutter --version

关于安装包结构的更多信息,请查看这个页面: Flutter 安装包结构

For additional details on how our installation bundles are structured, see Installation bundles.

每次版本发布我们也都会在微博上发布一条信息,欢迎关注 Flutter社区 微博账号!

We will post a Weibo message with each Flutter releases, please follow us on Weibo: Flutter Community!

破坏性变更 (Breaking changes)

目录

正如 重要改动策略 中描述的,我们会不定期地发布关于重要改动的迁移指南。

As described in the breaking change policy, on occasion we publish guides for migrating code across a breaking change.

以下是可用的迁移指南,它们按发行版本分类并按字母顺序排列。

The following guides are available. They are sorted by release, and listed in alphabetical order:

尚未推进到稳定版

Not yet released to stable

已经在 Flutter 2.5 中发布

Released in Flutter 2.5

在 2.2 版本中回退的改动

Reverted change in 2.2

以下重要改动在 Flutter 2.2 中已被回退。

The following breaking change was reverted in release 2.2:

iOS 端和 Android 端的网络策略
引入版本:2.0.0
回退版本:2.2.0(建议)

Network Policy on iOS and Android
Introduced in version: 2.0.0
Reverted in version: 2.2.0 (proposed)

发布于 Flutter 2.2

Released in Flutter 2.2

发布于 Flutter 2

Released in Flutter 2

发布于 Flutter 1.22

Released in Flutter 1.22

发布于 Flutter 1.20

Released in Flutter 1.20

发布于 Flutter 1.17

Released in Flutter 1.17

Flutter 发行说明

This page links to announcements and release notes for releases to the stable channel.

Flutter and the pubspec file

目录

Every Flutter project includes a pubspec.yaml file, often referred to as the pubspec. A basic pubspec is generated when you create a new Flutter project. It’s located at the top of the project tree and contains metadata about the project that the Dart and Flutter tooling needs to know. The pubspec is written in YAML, which is human readable, but be aware that white space (tabs v spaces) matters.

The pubspec file specifies dependencies that the project requires, such as particular packages (and their versions), fonts, or image files. It also specifies other requirements, such as dependencies on developer packages (like testing or mocking packages), or particular constraints on the version of the Flutter SDK.

Fields common to both Dart and Flutter projects are described in the pubspec file on dart.dev. This page lists Flutter-specific fields that are only valid for a Flutter project.

When you create a new project with the flutter create command (or by using the equivalent button in your IDE), it creates a pubspec for a basic Flutter app.

Here is an example of a Flutter project pubspec file. The Flutter only fields are highlighted.

name: <project name>
description: A new Flutter project.

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:       # Required for every Flutter project
    sdk: flutter # Required for every Flutter project

  cupertino_icons: ^1.0.2 # Only required if you use Cupertino (iOS style) icons

dev_dependencies:
  flutter_test:
    sdk: flutter # Required for a Flutter project that includes tests

flutter:

  uses-material-design: true # Required if you use the Material icon font

  assets:  # Lists assets, such as image files
    - images/a_dot_burr.jpeg
    - images/a_dot_ham.jpeg

  fonts:              # Required if your app uses custom fonts
    - family: Schyler
      fonts:
        - asset: fonts/Schyler-Regular.ttf
        - asset: fonts/Schyler-Italic.ttf
          style: italic
    - family: Trajan Pro
      fonts:
        - asset: fonts/TrajanPro.ttf
        - asset: fonts/TrajanPro_Bold.ttf
          weight: 700

Assets

Common types of assets include static data (for example, JSON files), configuration files, icons, and images (JPEG, WebP, GIF, animated WebP/GIF, PNG, BMP, and WBMP).

Besides listing the images that are included in the app package, an image asset can also refer to one or more resolution-specific “variants”. For more information, see the resolution aware section of the Assets and images page. For information on adding assets from package dependencies, see the asset images in package dependencies section in the same page.

Fonts

As shown in the above example, each entry in the fonts section should have a family key with the font family name, and a fonts key with a list specifying the asset and other descriptors for the font.

For examples of using fonts see the Use a custom font and Export fonts from a package recipes in the Flutter cookbook.

More information

For more information on packages, plugins, and pubspec files, see the following:

热重载 (Hot reload)

目录

Flutter 的热重载功能可帮助您在无需重新启动应用程序的情况下快速、轻松地测试、构建用户界面、添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便您可以快速查看更改的效果。

Flutter’s hot reload feature helps you quickly and easily experiment, build UIs, add features, and fix bugs. Hot reload works by injecting updated source code files into the running Dart Virtual Machine (VM). After the VM updates classes with the new versions of fields and functions, the Flutter framework automatically rebuilds the widget tree, allowing you to quickly view the effects of your changes.

如何热重载:

How to perform a hot reload

热重载 Flutter 应用:

To hot reload a Flutter app:

  1. 在支持 Flutter 编辑器 或终端窗口运行应用程序,物理机或虚拟器都可以。 Flutter 应用程序只有在调试模式下才能被热重载。

    Run the app from a supported Flutter editor or a terminal window. Either a physical or virtual device can be the target. Only Flutter apps in debug mode can be hot reloaded.

  2. 修改项目中的一个Dart文件。大多数类型的代码更改可以热重载;一些需要重新启动应用程序的更改列表,请参阅 特别情况

    Modify one of the Dart files in your project. Most types of code changes can be hot reloaded; for a list of changes that require a hot restart, see Special cases.

  3. 如果您在支持 Flutter IDE 工具的 IDE /编辑器中工作,请选择 Save All (cmd-s/ctrl-s),或单击工具栏上的 Hot Reload 按钮。

    If you’re working in an IDE/editor that supports Flutter’s IDE tools, select Save All (cmd-s/ctrl-s), or click the hot reload button on the toolbar.

    如果您正在使用命令行 flutter run 运行应用程序,请在终端窗口输入 r

    If you’re running the app at the command line using flutter run, enter r in the terminal window.

成功执行热重载后,您将在控制台中看到类似于以下内容的消息:

After a successful hot reload operation, you’ll see a message in the console similar to:

Performing hot reload...
Reloaded 1 of 448 libraries in 978ms.

应用程序更新以反映您的更改,并且应用程序的当前状态将保留。您的应用程序将继续从之前运行热重载命令的位置开始执行。代码被更新并继续执行。

The app updates to reflect your change, and the current state of the app is preserved. Your app continues to execute from where it was prior to running the hot reload command. The code updates and execution continues.

Android Studio UI

Android Studio 中的运行、运行调试、热重载和热重启的控件位置

Controls for run, run debug, hot reload, and hot restart in Android Studio

只有修改后的 Dart 代码再次运行时,代码更改才会产生可见效果。具体来说,热重载会导致所有现有的 widgets 重新构建。只有与 widgets 重新构建相关的代码才会自动重新执行。 main() and initState() 方法则不会再次运行。

A code change has a visible effect only if the modified Dart code is run again after the change. Specifically, a hot reload causes all of the existing widgets to rebuild. Only code involved in the rebuilding of the widgets is automatically re-executed. The main() and initState() functions, for example, are not run again.

特别情况

Special cases

下面的部分会描述一些热重载的特别的情况。在某些情况下,对 Dart 代码的小改动将确保您能够继续使用热重载。在其他情况下,需要热重启或完全重启。

The next sections describe specific scenarios that involve hot reload. In some cases, small changes to the Dart code enable you to continue using hot reload for your app. In other cases, a hot restart, or a full restart is needed.

应用被杀死

An app is killed

热重载会在应用被杀死之后断掉。比如一直在后台运行的应用(会被系统杀死)。

Hot reload can break when the app is killed. For example, if the app was in the background for too long.

编译错误

Compilation errors

当代码更改导致编译错误时,热重载会生成类似于以下内容的错误消息:

When a code change introduces a compilation error, hot reload generates an error message similar to:

Hot reload was rejected:
'/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': warning: line 16 pos 38: unbalanced '{' opens here
  Widget build(BuildContext context) {
                                     ^
'/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': error: line 33 pos 5: unbalanced ')'
    );
    ^

在这种情况下,只需更正上述代码的错误,即可以继续使用热重载。

In this situation, simply correct the errors on the specified lines of Dart code to keep using hot reload.

CupertinoTabView’s builder

Hot reload won’t apply changes made to a builder of a CupertinoTabView. For more information, see Issue 43574.

Enumerated types

Hot reload doesn’t work when enumerated types are changed to regular classes or regular classes are changed to enumerated types.

For example:

Before the change:

enum Color {
  red,
  green,
  blue,
}

After the change:

class Color {
  Color(this.i, this.j);
  final int i;
  final int j;
}

字体修改

Changing fonts

Hot reload supports changing assets, for the most part. However, if you change fonts, you’ll need to hot restart.

Generic types

Hot reload won’t work when generic type declarations are modified. For example, the following won’t work:

Before the change:

class A<T> {
  T? i;
}

After the change:

class A<T, V> {
  T? i;
  V? v;
}

Native code

If you’ve changed native code (such as Kotlin, Java, Swift, or Objective-C), you must perform a full restart (stop and restart the app) to see the changes take effect.

Previous state is combined with new code

Flutter’s stateful hot reload preserves the state of your app. This approach enables you to view the effect of the most recent change only, without throwing away the current state. For example, if your app requires a user to log in, you can modify and hot reload a page several levels down in the navigation hierarchy, without re-entering your login credentials. State is kept, which is usually the desired behavior.

If code changes affect the state of your app (or its dependencies), the data your app has to work with might not be fully consistent with the data it would have if it executed from scratch. The result might be different behavior after hot reload versus a hot restart.

代码发生更改但应用程序的状态没有改变

Recent code change is included but app state is excluded

在 Dart 中,静态字段会被惰性初始化。这意味着第一次运行 Flutter 应用程序并读取静态字段时,会将静态字段的值设为其初始表达式的结果。全局变量和静态字段都被视为状态,因此在热重载期间不会重新初始化。

In Dart, static fields are lazily initialized. This means that the first time you run a Flutter app and a static field is read, it is set to whatever value its initializer was evaluated to. Global variables and static fields are treated as state, and are therefore not reinitialized during hot reload.

如果更改全局变量和静态字段的初始化语句,则需要完全重启以查看更改。例如,参考以下代码:

If you change initializers of global variables and static fields, a full restart is necessary to see the changes. For example, consider the following code:

final sampleTable = [
  Table(
    children: const [
      TableRow(
        children: [Text('T1')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T2')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T3')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T4')],
      )
    ],
  ),
];

运行应用程序后,如果进行以下更改:

After running the app, you make the following change:

final sampleTable = [
  Table(
    children: const [
      TableRow(
        children: [Text('T1')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T2')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T3')],
      )
    ],
  ),
  Table(
    children: const [
      TableRow(
        children: [Text('T10')], // modified
      )
    ],
  ),
];

热重载后,这个改变并没有产生效果。

You hot reload, but the change is not reflected.

相反,在下面示例中:

Conversely, in the following example:

const foo = 1;
final bar = foo;
void onClick() {
  print(foo);
  print(bar);
}

第一次运行应用程序会打印 11。然后,如果您进行以下更改:

Running the app for the first time prints 1 and 1. Then, you make the following change:

const foo = 2; // modified
final bar = foo;
void onClick() {
  print(foo);
  print(bar);
}

热重载后,现在打印出 21。虽然对 const 定义的字段值的更改始终会重新加载,但不会重新运行静态字段的初始化语句。从概念上讲,const 字段被视为别名而不是状态。

While changes to const field values are always hot reloaded, the static field initializer is not rerun. Conceptually, const fields are treated like aliases instead of state.

Dart VM 在一组更改需要完全重启才能生效时,会检测初始化程序更改和标志。在上面的示例中,大部分初始化工作都会触发标记机制,但不适用于以下情况:

The Dart VM detects initializer changes and flags when a set of changes needs a hot restart to take effect. The flagging mechanism is triggered for most of the initialization work in the above example, but not for cases like the following:

final bar = foo;

为了能够更改 foo 并在热重载后查看更改,应该将字段重新用 const 定义或使用 getter 来返回值,而不是使用 final。例如下面的解决方案应该都可以使用:

To update foo and view the change after hot reload, consider redefining the field as const or using a getter to return the value, rather than using final. For example, either of the following solutions work:

const bar = foo;

或者:

or:

const foo = 1;
const bar = foo; // Convert foo to a const...
void onClick() {
  print(foo);
  print(bar);
}
const foo = 1;
int get bar => foo; // ...or provide a getter.
void onClick() {
  print(foo);
  print(bar);
}

了解更多 Dart 中关于 const 和 final 关键字的区别.

For more information, read about the differences between the const and final keywords in Dart.

用户界面没有改变

Recent UI change is excluded

即使热重载操作看起来成功了并且没有抛出异常,但某些代码更改可能在更新的 UI 中不可见。这种行为在更改应用程序的 main() 方法后很常见。

Even when a hot reload operation appears successful and generates no exceptions, some code changes might not be visible in the refreshed UI. This behavior is common after changes to the app’s main() or initState() methods.

作为一般规则,如果修改后的代码位于根 widget 的构建方法的下游,则热重载将按预期运行。但是,如果修改后的代码不会因重新构建 widget 树而重新执行的话,那么在热重载后您将看不到它的效果。

As a general rule, if the modified code is downstream of the root widget’s build() method, then hot reload behaves as expected. However, if the modified code won’t be re-executed as a result of rebuilding the widget tree, then you won’t see its effects after hot reload.

例如,参考以下代码:

For example, consider the following code:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(onTap: () => print('tapped'));
  }
}

运行应用程序后,你可能会像如下示例更改代码:

After running this app, change the code as follows:

import 'package:flutter/widgets.dart';

void main() {
  runApp(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
}

完全重启后,程序会从头开始执行新的 main() 方法,并构建一个 widget 树来显示文本 Hello

With a hot restart, the program starts from the beginning, executes the new version of main(), and builds a widget tree that displays the text Hello.

但是,如果您在更改后是通过热重载运行,main() 方法则不会重新执行,并且会使用未修改的 MyApp 实例作为根 widget 树来构建新的 widget 树,热重载后结果没有变化。

However, if you hot reload the app after this change, main() and initState() are not re-executed, and the widget tree is rebuilt with the unchanged instance of MyApp as the root widget. This results in no visible change after hot reload.

如何实现

How it works

调用热重载时,主机会查看自上次编译以来编辑的代码。重新编译以下文件:

When hot reload is invoked, the host machine looks at the edited code since the last compilation. The following libraries are recompiled:

这些库中的源代码被编译为 内核文件,并发送到移动设备的 Dart VM 中。

The source code from those libraries is compiled into kernel files and sent to the mobile device’s Dart VM.

Dart VM 重新加载新内核文件中的所有文件。到目前为止,没有重新执行代码。

The Dart VM re-loads all libraries from the new kernel file. So far no code is re-executed.

然后,热重载机制使 Flutter 框架触发所有现有的 widget 和渲染对象的重建/重新布局/重绘。

The hot reload mechanism then causes the Flutter framework to trigger a rebuild/re-layout/repaint of all existing widgets and render objects.

Flutter Fix

目录

Flutter 2 版本中引入的 Flutter Fix 功能将 Dart 命令行工具与 Dart analyzer 建议的更改相结合,用于自动清理代码库中已弃用的 API。

The Flutter Fix feature, introduced in Flutter 2, combines a Dart command-line tool with changes suggested by the Dart analyzer to automatically clean up deprecated APIs in your codebase.

此功能已被添加到 Flutter (2.0) 和 Dart (2.12) 的 IDE 插件中。这种自动更新的功能在 IntelliJ、Android Studio 和 Eclipse 中被称为 quick-fixes,在 VS Code 中被称为 code actions

This feature has also been added to IDE plugins for Flutter (2.0) and Dart (2.12). Depending on the IDE, these automated updates are called quick-fixes (IntelliJ, Android Studio, Eclipse) or code actions (VS Code).

应用单个修复

Applying individual fixes

你可以使用支持此功能的 IDE 逐个应用修复。

You can use any supported IDE to apply a single fix at a time.

IntelliJ 和 Android Studio

IntelliJ and Android Studio

当 analyzer 检测到已弃用的 API 时,该行代码上会出现一个灯泡状的图标。点击灯泡图标会显示将代码更新为新 API 的修复建议。点击建议的修复会执行 API 更新操作。

When the analyzer detects a deprecated API, a light bulb appears on that line of code. Clicking the light bulb displays the suggested fix that updates that code to the new API. Clicking the suggested fix performs the update.

在 IntelliJ 中使用 quick-fix 的一个案例。

Screenshot showing suggested change in IntelliJ
A sample quick-fix in IntelliJ

VS Code

当 analyzer 检测到已弃用的 API 时,它会提供一个报错信息。你可以执行以下任一操作:

When the analyzer detects a deprecated API, it presents an error. You can do any of the following:

在 VS Code 中使用 code action 的一个案例。

Screenshot showing suggested change in VS Code
A sample code action in VS Code

对整个工程应用修复

Applying project-wide fixes

你可以使用命令行工具 dart fix 来查看或应用整个项目的更改。

To see or apply changes to an entire project, you can use the command-line tool, dart fix.

此工具有两个可用选项:

This tool has two options:

更多有关 Flutter 废弃 API 的详细信息,请查看 Medium 上的 Flutter 废弃 API 的周期 文章。

For more information on Flutter deprecations, see Deprecation lifetime in Flutter, a free article on Flutter’s Medium publication.

代码格式化

目录

每个人都有自己喜欢的代码样式。但是根据我们的经验,下面这些做法可以提高团队的开发效率:

While your code might follow any preferred style—in our experience—teams of developers might find it more productive to:

如果没有统一的代码样式,当进行代码审查的时候,可能会为了一些样式的问题而进行争论从而浪费时间。代码审查最好把时间花在代码的行为上,而不是代码的样式上。

The alternative is often tiring formatting debates during code reviews, where time might be better spent on code behavior rather than code style.

在 Android Studio / IntelliJ 中自动格式化代码

Automatically formatting code in Android Studio and IntelliJ

在 Android Studio / IntelliJ 中安装 Dart 插件(见章节 编辑工具设定) 来进行代码的自动格式化。在当前代码窗口中格式化代码的方法是,在 Windows 和 Linux 系统里使用 Ctrl+Alt+L,在 Mac 系统里使用 Cmd+Alt+L。 Android Studio 和 IntelliJ 为 Flutter 页面提供了一个选项,即“在保存的时候格式化代码”—— Format code on save,要开启这个,可以在 Windows 和 Linux 下选择设置、在 Mac 下选择偏好设置。这样在每次保存代码的时候就会自动格式化当前文件。

Install the Dart plugin (see Editor setup) to get automatic formatting of code in Android Studio and IntelliJ. To automatically format your code in the current source code window, use Cmd+Alt+L (on Mac) or Ctrl+Alt+L (on Windows and Linux). Android Studio and IntelliJ also provides a check box named Format code on save on the Flutter page in Preferences (on Mac) or Settings (on Windows and Linux) which will format the current file automatically when you save it.

在 VS Code 中自动格式化代码

Automatically formatting code in VS Code

在 VS Code 中安装 Flutter 扩展(见章节 编辑工具设定)来进行代码的自动格式化。

Install the Flutter extension (see Editor setup) to get automatic formatting of code in VS Code.

格式化当前窗口中代码的方法是先在代码窗口中单击右键,然后选择 Format Document 选项即可。也可以在 VS Code 的偏好设置里面增加快捷键,然后使用快捷键操作。

To automatically format the code in the current source code window, right-click in the code window and select Format Document. You can add a keyboard shortcut to this VS Code Preferences.

editor.formatOnSave 设置成 true,可以在保存文件的时候自动进行代码格式化。

To automatically format code whenever you save a file, set the editor.formatOnSave setting to true.

使用 ‘flutter’ 命令自动格式化代码

Automatically formatting code with the ‘flutter’ command

我们也可以在命令行界面(CLI)中使用 flutter format 命令,进行代码的自动格式化。

You can also automatically format code in the command line interface (CLI) using the flutter format command:

$ flutter format path1 path2 ...

末尾处添加逗号

Using trailing commas

Flutter 代码经常会构建一定深度的树形数据结构,如在 build 方法中。为了有更好的自动格式化效果,我们推荐在末尾处添加逗号,尽管也可以不这样做。规则也比较简单:总是在函数、普通方法、构造方法参数列表的末尾处添加逗号。这样做会使格式化工具自动插入一些换行符,使代码更具有 Flutter 风格。

Flutter code often involves building fairly deep tree-shaped data structures, for example in a build method. To get good automatic formatting, we recommend you adopt the optional trailing commas. The guideline for adding a trailing comma is simple: Always add a trailing comma at the end of a parameter list in functions, methods, and constructors where you care about keeping the formatting you crafted. This helps the automatic formatter to insert an appropriate amount of line breaks for Flutter-style code.

自动格式化的时候,末尾处 加入 逗号的例子:

末尾处有逗号进行代码自动格式化的情况 (Automatically formatted code with trailing commas)

Here is an example of automatically formatted code with trailing commas:

同样的代码在进行自动格式化的时候,末尾处没有逗号的例子:

末尾处没有逗号进行代码自动格式化的情况 (Automatically formatted code without trailing commas)

And the same code automatically formatted code without trailing commas:

Web 渲染器

目录

你可以选择两种不同的渲染器来运行和构建 Web 应用。下文介绍两种渲染器以及它们的适用场景:

你可以选择两种不同的渲染器来运行和构建 Web 应用。下文介绍两种渲染器以及它们的适用场景。

When running and building apps for the web, you can choose between two different renderers. This page describes both renderers and how to choose the best one for your needs. The two renderers are:

使用 HTML 渲染
使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小。

HTML renderer
Uses a combination of HTML elements, CSS, Canvas elements, and SVG elements. This renderer has a smaller download size.

使用 CanvasKit 渲染
将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。

CanvasKit renderer
Uses Skia compiled to WebAssembly and rendered using WebGL. This renderer is fully consistent with Flutter mobile and desktop, has faster performance, and is less likely to have differences across browsers, but adds about 2MB in download size.

命令行参数

Command line options

--web-renderer 可选参数值为 autohtmlcanvaskit

The --web-renderer command line option takes one of three values, auto, html, or canvaskit.

此选项适用于 runbuild 命令。例如:

This flag can be used with the run or build subcommands. For example:

flutter run -d chrome --web-renderer html
flutter build web --web-renderer canvaskit

如果运行/构建目标是非浏览器设备(即移动设备或桌面设备),这个选项会被忽略。

This flag is ignored when a non-browser (mobile or desktop) device target is selected.

配置运行时

Runtime configuration

要覆写 web 实时渲染器请执行以下操作:

To override the web renderer at runtime:

  <script type="text/javascript">
    let useHtml = // ...
    if(useHtml) {
      window.flutterWebRenderer = "html";
    } else {
      window.flutterWebRenderer = "canvaskit";
    }
  </script>
  <script src="main.dart.js" type="application/javascript"></script>

Flutter engine 启动之后无法再在 main.dart.js 更换 web 渲染器。

The web renderer can’t be changed after the Flutter engine startup process begins in main.dart.js.

选择合适的渲染器

Choosing which option to use

如果您在移动端浏览器平台上更关心应用大小,而桌面端浏览器更关心性能,请选择 auto 选项(默认)。

Choose the auto option (default) if you are optimizing for download size on mobile browsers and optimizing for performance on desktop browsers.

如果您在移动端和桌面端都更关心应用大小,请选择 html 选项。

Choose the html option if you are optimizing download size over performance on both desktop and mobile browsers.

canvaskit:移动端和桌面端都更关心性能,和跨浏览器的像素级一致性。

Choose the canvaskit option if you are prioritizing performance and pixel-perfect consistency on both desktop and mobile browsers.

示例

Examples

在 Chrome 浏览器上使用默认 (auto) 渲染器运行:

Run in Chrome using the default renderer option (auto):

flutter run -d chrome

使用默认 (auto) 渲染器构建应用(发布模式):

Build your app in release mode, using the default (auto) option:

flutter build web --release

使用 CanvasKit 渲染器构建应用(发布模式):

Build your app in release mode, using just the CanvasKit renderer:

flutter build web --web-renderer canvaskit --release

使用 HTML 渲染器构建应用(发布模式):

Run your app in profile mode using the HTML renderer:

flutter run -d chrome --web-renderer html --profile

迁移到 AndroidX

目录

AndroidX 是针对 Android 原生支持库的重大改进。

AndroidX is a major improvement to the original Android Support Library.

其提供了包名为 androidx.*,且并未与平台 API 关联的类库,这意味着它提供了向后的兼容性,以及比 Android 平台更频繁的更新。

It provides the androidx.* package libraries, unbundled from the platform API. This means that it offers backward compatibility and is updated more frequently than the Android platform.

常见问题

Common Questions

如何将现有的应用程序、插件,或者可编辑的模块项目迁移至 AndroidX?

How do I migrate my existing app, plugin or host-editable module project to AndroidX?

你需要 Android Studio 3.2 或其更高的版本。若尚未安装,可从 Android Studio 页面下载最新的版本。

You will need Android Studio 3.2 or higher. If you don’t have it installed, you can download the latest version from the Android Studio site.

  1. 打开 Android Studio。

    Open Android Studio.

  2. 选中 Open an existing Android Studio Project

    Select Open an existing Android Studio Project.

  3. 在你的应用路径中打开 android 目录。

    Open the android directory within your app.

  4. 等待项目直到其同步成功。

    Wait until the project has been synced successfully.

(一旦打开项目,同步就会自动构建,若没有自动构建,请从 File 菜单中选中 Sync Project with Gradle Files)。

(This happens automatically once you open the project, but if it doesn’t, select Sync Project with Gradle Files from the File menu).

  1. Refactor 菜单中选择 Migrate to AndroidX

    Select Migrate to AndroidX from the Refactor menu.

  2. 在继续之前,若被要求对项目进行备份,选中 Backup project as Zip file ,然后单击 Migrate ,最终将 zip 文件保存在你喜欢的路径下。

    If you are asked to backup the project before proceeding, check Backup project as Zip file, then click Migrate. Lastly, save the zip file in your location of preference.

Select backup project as zip file

  1. 重构预览展示了变动的列表,最后,单击 Do Refactor

    The refactoring preview shows the list of changes. Finally, click Do Refactor:

An animation of the bottom-up page transition on Android

  1. 大功告成!你已成功将项目迁移到 AndroidX。

    That is it! You successfully migrated your project to AndroidX.

最后,如果你对插件进行了迁移,请发布新的 AndroidX 版本到 pub 并更新的 CHANGELOG.md,以指明该版本与 AndroidX 兼容。

Finally, if you migrated a plugin, publish the new AndroidX version to pub and update your CHANGELOG.md to indicate that this new version is compatible with AndroidX.

若无法使用 Android Studio 怎么办?

What if I can’t use Android Studio?

你可以使用 Flutter 工具创建一个新项目,然后将 Dart 代码和资源文件移动到新的项目中。

You can create a new project using the Flutter tool and then move the Dart code and assets to the new project.

要创建一个新的项目,请运行:

To create a new project run:

flutter create -t <project-type> <new-project-path>

添加至应用

Add to App

若的 Flutter 项目类型是用于添加至现有 Android 应用的模块,并且包含 .android 目录,则将下述代码添加至 pubspec.yaml 中。

If your Flutter project is a module type for adding to an existing Android app, and contains a .android directory, add the following line to pubspec.yaml:

 module:
   ...
    androidX: true # Add this line.

最后,运行 flutter clean

Finally, run flutter clean.

若你的模块中包含一个 android 目录,请按照上一节中的步骤执行。

If your module contains an android directory instead, then follow the steps in previous section.

如何判断我的项目中是否使用了 AndroidX?

How do I know if my project is using AndroidX?

自 Flutter 1.12.13 版本之后,使用 flutter create -t <project-type> 命令行创建的 Flutter 项目将会默认使用 AndroidX。

Starting from Flutter v1.12.13, new projects created with flutter create -t <project-type> use AndroidX by default.

在此 Flutter 版本(1.12.13)之前创建的项目不能依赖任何 工件映射类映射

Projects created prior to this Flutter version must not depend on any old build artifact or old Support Library class.

android/gradle.properties.android/gradle.properties 文件中需要包含下述代码:

In an app or module project, the file android/gradle.properties or .android/gradle.properties must contain:

android.useAndroidX=true
android.enableJetifier=true

若不将应用程序或模块迁移至 AndroidX 将会怎样?

What if I don’t migrate my app or module to AndroidX?

你的应用程序也许能继续运行。然而,通常不建议将 AndroidX 和 Support 组件结合起来使用,因为这会导致依赖关系冲突或者 Gradle 的其它类型失败。

Your app may continue to work. However, combining AndroidX and Support artifacts is generally not recommended because it can result in dependency conflicts or other kind of Gradle failures. As a result, as more plugins migrate to AndroidX, plugins depending on Android core libraries are likely to cause build failures.

若我将应用迁移到了 AndroidX,但我使用到的插件没有全部支持 AndroidX 怎么办?

What if my app is migrated to AndroidX, but not all of the plugins I use?

Flutter 工具使用 Jetifier 将支持库中的 Flutter 插件自动迁移到 AndroidX,因此,即使你尚未将其迁移到 AndroidX ,你也可以使用相同的插件。

The Flutter tool uses Jetifier to automatically migrate Flutter plugins using the Support Library to AndroidX, so you can use the same plugins even if they haven’t been migrated to AndroidX yet.

在迁移至 AndroidX 的过程中遇到了问题

I’m having issues migrating to AndroidX

在 GitHub 上创建一个问题 并为其添加一个 [androidx-migration] 标题。

Open an issue on GitHub and add [androidx-migration] to the title of the issue.

Flutter 的 iOS 14 支持

目录

The iOS 14 release, the new version of Apple’s mobile operating system, is here. This page describes some known issues when developing for iOS 14.

Launching Flutter with Flutter tools

Due to low-level changes in iOS’s debugger mechanisms, developers using versions of Flutter earlier than 1.20.4 stable won’t be able to launch apps (by using flutter run or a Flutter-enabled IDE) on physical iOS devices running iOS 14. This affects debug, profile, and release builds. Simulator builds, add-to-app modules, and running directly from Xcode are unaffected.

Upgrading to Flutter 1.22 beta allows you to build, test, and deploy to iOS without issue. Upgrading to 1.20.4 stable allows you to build and deploy to iOS 14, but not debug.

Clipboard notifications

If your iOS 14 app uses text fields, you should build your production apps with Flutter 1.20 or the 1.22 beta to ensure that clipboard access notifications are not spuriously shown when building text fields.

System font rendering

If your iOS 14 app uses system fonts such as San Francisco (used by default by Cupertino widgets), text will be incorrectly rendered in a condensed letter spacing due to changes in iOS’s font loading mechanism. This affects debug and production apps on iOS 14.

To ensure correct font rendering, you should build your production apps with Flutter 1.22 beta.

Debugging Flutter

Due to added security around local network permissions in iOS 14, a permission dialog box must now be accepted for each application in order to enable Flutter debugging functionalities such as hot-reload and DevTools.

Screenshot of "allow network connections" dialog

This affects debug and profile builds only and won’t appear in release builds. The permission can also be allowed by enabling Settings > Privacy > Local Network > Your App.

For add-to-app users, one additional step has been added to the add-to-app project setup guide, to re-enable flutter attach for debug builds on physical devices on iOS 14.

Launching debug Flutter without a host computer

Also due to changes in debugger mechanisms, once a Flutter debug application is installed on the device (either by using flutter run a Flutter-enabled IDE, or from Xcode), the application can no longer be re-launched by tapping the application’s icon in the home screen in iOS 14 on physical devices.

Other launch paths without a host computer, such as deep links or notifications, won’t work on iOS 14 physical devices in debug mode.

Add-to-app debug mode modules will crash on iOS 14 physical devices when running the FlutterEngine if the host application is launched from the home screen.

To launch the application in debug mode on a physical device again, re-run the app from the host computer (by using flutter run, a Flutter-enabled IDE, or Xcode).

You can also build the application or add-to-app module in profile or release mode, or on a simulator, which are not affected.

See Issue 60657 for more details.

Conclusion

You might also be interested in the following tracking bugs:

If you experience other bugs or unpolished edges when developing for iOS 14, please file a bug!

Xcode 迁移

To develop Flutter apps for iOS, you need a Mac with Xcode installed. Xcode 11.4 changed the way frameworks are linked and embedded, and you may see the following errors when switching between iOS devices and simulators:

Building for iOS, but the linked and embedded framework 'App.framework' was built for iOS Simulator.

or

Building for iOS Simulator, but the linked and embedded framework 'App.framework' was built for iOS.

Flutter v1.15.3 and later automatically migrates your Xcode project.

If you need to manually upgrade your project, use the following steps:

  1. From the Flutter app directory, open ios/Runner.xcworkspace in Xcode.

  2. In the Navigator pane, locate the Flutter group and remove App.framework and Flutter.framework.

    Remove Frameworks in Xcode Navigator
  3. In the Runner target build settings Build Phases > Link Binary With Libraries confirm App.framework and Flutter.framework are no longer present. Also confirm in Build Phases > Embed Frameworks.

    Confirm Frameworks Removed from Build Phases
  4. Change the Runner target build settings Build Phases > Thin Binary script as follows:

    /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
    /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" thin
    
    Update Thin Binary Script Build Phase
  5. In the Runner target Build Settings > Other Linker Flags (OTHER_LDFLAGS) add $(inherited) -framework Flutter.

    Update Other Linker Arguments Build Setting

调试 Flutter 应用

目录

有很多工具和特性可以帮助调试 Flutter 应用程序,如下列举了一些:

There’s a wide variety of tools and features to help debug Flutter applications. Here are some of the available tools:

开发者工具

DevTools

要调试及分析应用,开发者工具可能是你的首选。开发者工具运行在浏览器,支持以下特性:

For debugging and profiling apps, DevTools might be the first tool you reach for. DevTools runs in a browser and supports a variety of features:

如果你在 debug 模式profile 模式 运行,那么可以在浏览器打开开发者工具连接到你的应用。开发者工具不能用在以 release 模式 编译的应用,因为调试和分析信息都被删除了。

If you run your application in debug mode or profile mode, while it’s running you can open DevTools in the browser to connect to your app. DevTools doesn’t work well with an app compiled to release mode, as the debugging and profiling information has been stripped away.

如果你要用开发者工具分析应用,需确保使用 性能模式。否则,分析的主要输出将会是用于验证框架中各种不变式的调试断言(查看 debug 模式断言)。

If you use DevTools for profiling, make sure to run your application in profile mode. Otherwise, the main output that appears on your profile are the debug asserts verifying the framework’s various invariants (see Debug mode assertions).

GIF showing DevTools features

想获取更多信息,请查看 开发者工具 文档。

For more information, see the DevTools documentation.

设置断点

Setting breakpoints

要设置断点,可以直接在 IDE 或编辑器(比如 Android Studio/IntelliJVS Code)、 开发者工具调试器 设置,或者 通过编码的方式设置

You can set breakpoints directly in your IDE/editor (such as Android Studio/IntelliJ and VS Code), in the DevTools debugger, or programmatically.

Dart 分析器

The Dart analyzer

如果你使用的是 Flutter 推荐的 IDE 或编辑器,则自带的 Dart 分析器默认会检查代码,并发现可能的错误。

If you’re using a Flutter enabled IDE/editor, the Dart analyzer is already checking your code and looking for possible mistakes.

如果你使用命令行,则可以使用 flutter analyze 检查代码。

If you run from the command line, test your code with flutter analyze.

Dart 分析器非常依赖你在代码中添加的类型注解,以帮助跟踪问题。建议您在各个地方都加上注解(避免 var,无类型参数,无类型 list 字面量,等等),因为这是跟踪问题最快且最不痛苦的方式。

The Dart analyzer makes heavy use of type annotations that you put in your code to help track problems down. You are encouraged to use them everywhere (avoiding var, untyped arguments, untyped list literals, and so on) as this is the quickest and least painful way of tracking down problems.

想获取更多信息,请查看 使用 Dart 分析器

For more information, see Using the Dart analyzer.

日志

Logging

另一个有用的调试工具是日志。通过 编码 配置日志,然后在开发者工具中的 日志视图 或控制台查看输出。

Another useful debugging tool is logging. You set logging up programmatically then view the output in the DevTools logging view, or in the console.

调试应用层

Debugging application layers

Flutter 采用分层架构,包括 widget、渲染和绘制等层。想获取更多信息和视频,请查看 GitHub wiki 上的 The Framework architecture,和社区文章 The Layer Cake

Flutter was designed with a layered architecture that includes widget, rendering, and painting layers. For links to more information and videos, see The Framework architecture on the GitHub wiki, and the community article, The Layer Cake.

Flutter widget 检查器提供了 widget 树的视觉展现,如果你想要更多细节,或关于 wiget、层级或渲染树的详尽文本转储,请查看 添加输出代码的方式调试 Flutter 应用 页面的 调试标志:应用层 部分。

The Flutter widget inspector provides a visual representation of the widget tree, but if you want a greater level of detail, or you want a verbose text-based dump of the widget, layer, or render trees, see Debug flags: application layers in the Debugging Flutter apps programmatically page.

Debug 模式断言

Debug mode assertions

在开发过程中,强烈建议您使用 Flutter 的 debug 模式。如果你是用 Android Studio 的 bug 图标运行,或者在命令行执行 flutter run,则默认会使用 debug 模式。有些工具通过 --enable-assets 命令行标志可以支持断言语句。

During development, you are highly encouraged to use Flutter’s debug mode. This is the default if you use bug icon in Android Studio, or flutter run at the command line. Some tools support assert statements through the command-line flag --enable-asserts.

在此模式,Dart 断言语句被开启, Flutter 框架在执行时会计算每一个遇到的断言语句的参数,当结果是 false 时抛出异常。如此一来,开发者可以控制不变式检查的开启或关闭,相应的性能损耗将只发生在调试期间。

In this mode, Dart assert statements are enabled, and the Flutter framework evaluates the argument to each assert statement encountered during execution, throwing an exception if the result is false. This allows developers to enable or disable invariant checking, such that the associated performance cost is only paid during debugging sessions.

有不变式被违反时,它会被报告给控制台,并携带一些帮助跟踪问题源的上下文信息。

When an invariant is violated, it’s reported to the console, with some context information to help track down the source of the problem.

想获取更多信息,请查看 探索 Dart 语言 中的 断言 部分。

For more information, see Assert in the Dart language tour.

调试动画

Debugging animations

调试动画最简单的方法是让它们变慢。 Flutter inspector 提供一个 放慢动画(Slow Animations) 的按钮,你也可以 在代码中放慢动画

The easiest way to debug animations is to slow them down. The Flutter inspector provides a Slow Animations button, or you can slow the animations programmatically.

想获取更多关于调试动画卡顿的信息,请查看 Flutter 性能分析

For more information on debugging janky (non-smooth) applications, see Flutter performance profiling.

Measuring app startup time

测量应用启动时间

要收集有关 Flutter 应用程序启动所需时间的详细信息,可以在运行 flutter run 时使用 trace-startupprofile 选项。

To gather detailed information about the time it takes for your Flutter app to start, you can run the flutter run command with the trace-startup and profile options.

$ flutter run --trace-startup --profile

跟踪输出被保存到 Flutter 工程目录在 build 目录下,一个名为 start_up_info.json 的 JSON 文件中。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:

The trace output is saved as a JSON file called start_up_info.json under the build directory of your Flutter project. The output lists the elapsed time from app startup to these trace events (captured in microseconds):

例如:

For example:

{
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}

Tracing Dart code

跟踪 Dart 代码性能

要进行性能跟踪,你可以使用开发者工具的 时间线视图。时间线视图还支持导入和导出跟踪文件。想要获取更多信息,请查看 时间线视图

To perform a performance trace, you can use the DevTools Timeline view. The Timeline view also supports importing and exporting trace files. For more information, see the Timeline view docs.

你也可以 在代码中跟踪,不过这些跟踪信息无法导入到开发者模式的时间线视图。

You can also perform traces programmatically, though these traces can’t be imported into DevTool’s Timeline view.

跟踪时请确保在 性能模式 运行应用,这样才能保证运行时性能特征同你最终产品高度一致。

Be sure to use run your app in profile mode before tracing to ensure that the runtime performance characteristics closely matches that of your final product.

性能图层

Performance overlay

要图形化展现你应用的性能,可以开启性能图层。你可以在 Flutter inspector 中点击 Performance Overlay 按钮。

To get a graphical view of the performance of your application, turn on the performance overlay. You can do this in the by clicking the Performance Overlay button in the Flutter inspector.

你也可以 在代码中 开启该图层。

You can also turn on the overlay programmatically.

关于如何解析图层中的图形,请查看 Flutter 性能分析 中的 性能图层 部分。

For information on how to interpret the graphs in the overlay, see The performance overlay in the Flutter performance profiling guide.

调试标志

Debug flags

大部分情况,你不需要直接使用调试标志,因为可以在 开发者工具 找到最有用的调试功能。但是如果你偏好直接使用调试标志,请查看 添加输出代码的方式调试 Flutter 应用 中的 调试标志:性能 部分。

In most cases, you won’t need to use the debug flags directly, as you’ll find the most useful debugging functionality in the DevTools suite. But if you prefer to use the debug flags directly, see Debug flags: performance in the Debugging Flutter apps programmatically page.

常见问题

Common problems

下面是一些在 macOS 上遇到的问题。

The following is a problem that some have encountered on macOS.

“句柄数超出系统限制” 异常 (macOS)

“Too many open files” exception (macOS)

mac OS 在同一时间可以打开多少句柄的默认限制数相当低。如果你达到这个极限,可以用 ulimit 命令增加可用句柄的数量:

The default limit for Mac OS on how many files it can have open at a time is rather low. If you run into this limit, increase the number of available file handlers using the ulimit command:

ulimit -S -n 2048

如果您使用 Travis 或 Cirrus 进行测试,请通过在 flutter/.travis.yml 或 flutter/.cirrus.yml 中增加同样的命令来增加它们可以打开的句柄数量。

If you use Travis or Cirrus for testing, increase the number of available file handlers that they can open by adding the same line to flutter/.travis.yml, or flutter/.cirrus.yml, respectively.

被标记为 const 的相同 Widget 应被视为同一对象,然而却并没有

Widgets marked const that should be equal to each other, aren’t

在 debug 模式下,(由于 Dart 的常量去重策略)你也许会发现两个 const 的 widget 长得并不完全一样。

In debug mode, you may find that two const widgets that should to all appearances be equal (because of Dart’s constant deduplication) are not.

例如,下面的代码应该打印 1:

For example, this code should print 1:

print(<Widget>{ // this is the syntax for a Set<Widget> literal
  const SizedBox(),
  const SizedBox(),
}.length);

这段代码应该打印 1(而不是 2),这是由于两个常量相同且在同一个 set 中(实际上分析器抱怨 “集合文字中的两个元素不应相等”)。正如我们所期待的那样,在 release 模式下构建的时候,它确实打印了 1。然而,在 debug 模式下它却打印了 2。这是由于 Flutter tool 在编译期向 Widget 的构造器注入了源位置,所以下面的代码有效:

It should print 1 (rather than 2) because the two constants are the same and sets coalesce duplicate values (and indeed the analyzer complains that “Two elements in a set literal shouldn’t be equal”). As expected, in release builds, it does print 1. However, in debug builds it prints 2. This is because the flutter tool injects the source location of Widget constructors into the code at compile time, so the code is effectively:

print(<Widget>{
  const SizedBox(location: Location(file: 'foo.dart', line: 12)),
  const SizedBox(location: Location(file: 'foo.dart', line: 13)),
}.length);

上面的代码在结果中的实例不同,故它们在 set 中并没有重复。我们使用注入信息汇报相关 widget 的创建信息,使得 widget 出现异常时错误消息会更加清晰。不幸的是,它会导致相同常量在编译期变为不同实例。

This results in the instances being different, and so they are not deduplicated by the set. We use this injected information to make the error messages clearer when a widget is involved in an exception, by reporting where the relevant widget was created. Unfortunately, it has the visible side-effect of making otherwise-identical constants be different at compile time.

要关闭此行为,请在运行 flutter run 命令的同时传 --no-track-widget-creation。有了这个标记,代码将会在 debug 和 release 模式下打印 1,而错误消息这边会有一条消息说,除非打开 widget 创建跟踪器,否则我们将无法提供完整的信息。

To disable this behavior, pass --no-track-widget-creation to the flutter run command. With that flag set, the code above prints “1” in debug and release builds, and error messages include a message saying that they cannot provide all the information that they would otherwise be able to provide if widget creation tracking was enabled.

你也可以查看:

See also:

其他资源

Other resources

以下是其他一些有用的文档:

You might find the following docs useful:

添加输出代码的方式调试 Flutter 应用

目录

这篇文章描述了如何在代码中启用调试功能。如果想了解整个调试和分析工具,可参见 Debugging 页面.

This doc describes debugging features that you can enable in code. For a full list of debugging and profiling tools, see the Debugging page.

日志输出

Logging

在应用中有两种日志输出方式。第一种方式是使用 stdoutstderr。通常,我们使用 print() 语句或者通过引入 dart:io 并且调用 stderrstdout 中的方法。如下:

You have two options for logging for your application. The first is to use stdout and stderr. Generally, this is done using print() statements, or by importing dart:io and invoking methods on stderr and stdout. For example:

stderr.writeln('print me');

如果您一次输出太多,Android 有时可能会丢失一些日志行。可以使用 Flutter 的 foundation 包中的 debugPrint() 方法来避免这个问题。它封装了 print 方法,通过控制输出的等级,从而避免输出内容被 Android 的内核丢弃。

If you output too much at once, then Android sometimes discards some log lines. To avoid this, use debugPrint(), from Flutter’s foundation library. This is a wrapper around print that throttles the output to a level that avoids being dropped by Android’s kernel.

另一种应用日志输出的方式是使用 dart:developer 中的 log() 方法。通过这种方式,您可以在输出日志中包含更精细化的信息。如下面这个示例:

The other option for application logging is to use the dart:developer log() function. This allows you to include a bit more granularity and information in the logging output. Here’s an example:

import 'dart:developer' as developer;

void main() {
  developer.log('log me', name: 'my.app.category');

  developer.log('log me 1', name: 'my.other.category');
  developer.log('log me 2', name: 'my.other.category');
}

您也可以在打印日志时传入应用数据。通常,在调用 log() 时也会使用命名参数 error:,您可以通过 JSON 编码想要传入的对象,并将编码后的字符串传给 error 参数。

You can also pass application data to the log call. The convention for this is to use the error: named parameter on the log() call, JSON encode the object you want to send, and pass the encoded string to the error parameter.

import 'dart:convert';
import 'dart:developer' as developer;

void main() {
  var myCustomObject = ...;

  developer.log(
    'log me',
    name: 'my.app.category',
    error: jsonEncode(myCustomObject),
  );
}

如果在 DevTool 的 logging 页面中查看日志输出情况, JSON 编码的错误参数会被解释为一个数据对象,并呈现在该日志条目的 details 视图中。

If viewing the logging output in DevTool’s logging view, the JSON encoded error param is interpreted as a data object and rendered in the details view for that log entry.

设置断点

Setting breakpoints

您可以使用 debugger() 语句插入编程式断点。在此之前,您需要在相关文件顶部引入 dart:developer 包。

You can insert programmatic breakpoints using the debugger() statement. To use this, you have to import the dart:developer package at the top of the relevant file.

debugger() 语句有一个可选参数 when,用来指定该断点触发的特定条件,如下这个示例:

The debugger() statement takes an optional when argument that you can specify to only break when a certain condition is true, as in the following example:

import 'dart:developer';

void someFunction(double offset) {
  debugger(when: offset > 30.0);
  // ...
}

Debug 标识:应用程序层

Debug flags: application layers

Flutter 框架的每个 layer 都提供了一个函数,用来将其当前状态或事件转储到控制台(使用 debugPrint)。

Each layer of the Flutter framework provides a function to dump its current state or events to the console (using debugPrint).

Widget 树

Widget tree

可以通过调用 debugDumpApp() 方法转储 widget 库的状态,如果应用已至少构建了一次,并且正处于调试模式时(runApp() 调用后的任何时间)。只要应用不在运行构建阶段,您可以调用随意该方法(也就是说,不能在 build() 方法中使用它)。

To dump the state of the Widgets library, call debugDumpApp(). You can call this more or less any time that the application is not in the middle of running a build sphae (in other words, not anywhere inside a build() method), if the app has built at least once and is in debug mode (in other words, any time after calling runApp()).

如下面这个应用:

For example, the following application:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: AppHome(),
    ),
  );
}

class AppHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: TextButton(
          onPressed: () {
            debugDumpApp();
          },
          child: Text('Dump App'),
        ),
      ),
    );
  }
}

上面应用的输出内容如下(具体细节因框架版本、设备大小等会有所差异):

The previous app outputs something like the following (the precise details vary by the version of the framework, the size of the device, and so forth):

I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559): └MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559):  └ScrollConfiguration()
I/flutter ( 6559):   └AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559):    └Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559):     └WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559):      └CheckedModeBanner()
I/flutter ( 6559):       └Banner()
I/flutter ( 6559):        └CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559):         └DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559):          └MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559):           └LocaleQuery(null)
I/flutter ( 6559):            └Title(color: Color(0xff2196f3))
I/flutter ( 6559):             └Navigator([GlobalObjectKey<NavigatorState> _WidgetsAppState(552902158)]; state: NavigatorState(240327618; tracking 1 ticker))
I/flutter ( 6559):              └Listener(listeners: down, up, cancel; behavior: defer-to-child; renderObject: RenderPointerListener)
I/flutter ( 6559):               └AbsorbPointer(renderObject: RenderAbsorbPointer)
I/flutter ( 6559):                └Focus([GlobalKey 489139594]; state: _FocusState(739584448))
I/flutter ( 6559):                 └Semantics(container: true; renderObject: RenderSemanticsAnnotations)
I/flutter ( 6559):                  └_FocusScope(this scope has focus; focused subscope: [GlobalObjectKey MaterialPageRoute<void>(875520219)])
I/flutter ( 6559):                   └Overlay([GlobalKey 199833992]; state: OverlayState(619367313; entries: [OverlayEntry@248818791(opaque: false; maintainState: false), OverlayEntry@837336156(opaque: false; maintainState: true)]))
I/flutter ( 6559):                    └_Theatre(renderObject: _RenderTheatre)
I/flutter ( 6559):                     └Stack(renderObject: RenderStack)
I/flutter ( 6559):                      ├_OverlayEntry([GlobalKey 612888877]; state: _OverlayEntryState(739137453))
I/flutter ( 6559):                      │└IgnorePointer(ignoring: false; renderObject: RenderIgnorePointer)
I/flutter ( 6559):                      │ └ModalBarrier()
I/flutter ( 6559):                      │  └Semantics(container: true; renderObject: RenderSemanticsAnnotations)
I/flutter ( 6559):                      │   └GestureDetector()
I/flutter ( 6559):                      │    └RawGestureDetector(state: RawGestureDetectorState(39068508; gestures: tap; behavior: opaque))
I/flutter ( 6559):                      │     └_GestureSemantics(renderObject: RenderSemanticsGestureHandler)
I/flutter ( 6559):                      │      └Listener(listeners: down; behavior: opaque; renderObject: RenderPointerListener)
I/flutter ( 6559):                      │       └ConstrainedBox(BoxConstraints(biggest); renderObject: RenderConstrainedBox)
I/flutter ( 6559):                      └_OverlayEntry([GlobalKey 727622716]; state: _OverlayEntryState(279971240))
I/flutter ( 6559):                       └_ModalScope([GlobalKey 816151164]; state: _ModalScopeState(875510645))
I/flutter ( 6559):                        └Focus([GlobalObjectKey MaterialPageRoute<void>(875520219)]; state: _FocusState(331487674))
I/flutter ( 6559):                         └Semantics(container: true; renderObject: RenderSemanticsAnnotations)
I/flutter ( 6559):                          └_FocusScope(this scope has focus)
I/flutter ( 6559):                           └Offstage(offstage: false; renderObject: RenderOffstage)
I/flutter ( 6559):                            └IgnorePointer(ignoring: false; renderObject: RenderIgnorePointer)
I/flutter ( 6559):                             └_MountainViewPageTransition(animation: AnimationController(⏭ 1.000; paused; for MaterialPageRoute<void>(/))➩ProxyAnimation➩Cubic(0.40, 0.00, 0.20, 1.00)➩Tween<Offset>(Offset(0.0, 1.0) → Offset(0.0, 0.0))➩Offset(0.0, 0.0); state: _AnimatedState(552160732))
I/flutter ( 6559):                              └SlideTransition(animation: AnimationController(⏭ 1.000; paused; for MaterialPageRoute<void>(/))➩ProxyAnimation➩Cubic(0.40, 0.00, 0.20, 1.00)➩Tween<Offset>(Offset(0.0, 1.0) → Offset(0.0, 0.0))➩Offset(0.0, 0.0); state: _AnimatedState(714726495))
I/flutter ( 6559):                               └FractionalTranslation(renderObject: RenderFractionalTranslation)
I/flutter ( 6559):                                └RepaintBoundary(renderObject: RenderRepaintBoundary)
I/flutter ( 6559):                                 └PageStorage([GlobalKey 619728754])
I/flutter ( 6559):                                  └_ModalScopeStatus(active)
I/flutter ( 6559):                                   └AppHome()
I/flutter ( 6559):                                    └Material(MaterialType.canvas; elevation: 0; state: _MaterialState(780114997))
I/flutter ( 6559):                                     └AnimatedContainer(duration: 200ms; has background; state: _AnimatedContainerState(616063822; ticker inactive; has background))
I/flutter ( 6559):                                      └Container(bg: BoxDecoration())
I/flutter ( 6559):                                       └DecoratedBox(renderObject: RenderDecoratedBox)
I/flutter ( 6559):                                        └Container(bg: BoxDecoration(backgroundColor: Color(0xfffafafa)))
I/flutter ( 6559):                                         └DecoratedBox(renderObject: RenderDecoratedBox)
I/flutter ( 6559):                                          └NotificationListener<LayoutChangedNotification>()
I/flutter ( 6559):                                           └_InkFeature([GlobalKey ink renderer]; renderObject: _RenderInkFeatures)
I/flutter ( 6559):                                            └AnimatedDefaultTextStyle(duration: 200ms; inherit: false; color: Color(0xdd000000); family: "Roboto"; size: 14.0; weight: 400; baseline: alphabetic; state: _AnimatedDefaultTextStyleState(427742350; ticker inactive))
I/flutter ( 6559):                                             └DefaultTextStyle(inherit: false; color: Color(0xdd000000); family: "Roboto"; size: 14.0; weight: 400; baseline: alphabetic)
I/flutter ( 6559):                                              └Center(alignment: Alignment.center; renderObject: RenderPositionedBox)
I/flutter ( 6559):                                               └TextButton()
I/flutter ( 6559):                                                └MaterialButton(state: _MaterialButtonState(398724090))
I/flutter ( 6559):                                                 └ConstrainedBox(BoxConstraints(88.0<=w<=Infinity, h=36.0); renderObject: RenderConstrainedBox relayoutBoundary=up1)
I/flutter ( 6559):                                                  └AnimatedDefaultTextStyle(duration: 200ms; inherit: false; color: Color(0xdd000000); family: "Roboto"; size: 14.0; weight: 500; baseline: alphabetic; state: _AnimatedDefaultTextStyleState(315134664; ticker inactive))
I/flutter ( 6559):                                                   └DefaultTextStyle(inherit: false; color: Color(0xdd000000); family: "Roboto"; size: 14.0; weight: 500; baseline: alphabetic)
I/flutter ( 6559):                                                    └IconTheme(color: Color(0xdd000000))
I/flutter ( 6559):                                                     └InkWell(state: _InkResponseState<InkResponse>(369160267))
I/flutter ( 6559):                                                      └GestureDetector()
I/flutter ( 6559):                                                       └RawGestureDetector(state: RawGestureDetectorState(175370983; gestures: tap; behavior: opaque))
I/flutter ( 6559):                                                        └_GestureSemantics(renderObject: RenderSemanticsGestureHandler relayoutBoundary=up2)
I/flutter ( 6559):                                                         └Listener(listeners: down; behavior: opaque; renderObject: RenderPointerListener relayoutBoundary=up3)
I/flutter ( 6559):                                                          └Container(padding: EdgeInsets(16.0, 0.0, 16.0, 0.0))
I/flutter ( 6559):                                                           └Padding(renderObject: RenderPadding relayoutBoundary=up4)
I/flutter ( 6559):                                                            └Center(alignment: Alignment.center; widthFactor: 1.0; renderObject: RenderPositionedBox relayoutBoundary=up5)
I/flutter ( 6559):                                                             └Text("Dump App")
I/flutter ( 6559):                                                              └RichText(renderObject: RenderParagraph relayoutBoundary=up6)

这是一个「被拉平的树」,通过它们的各种 build 函数,显示出所有 widget 信息。(如果您调用根 widget 的 toStringDeep() 方法,就会得到这棵树。)您会看到很多 widget ,虽然它们没出现在应用的源码中,但却出现在这颗树中,因为它们是由框架中 widget 的 build 函数插入的。比如,Material widget 的实现细节中就包括了 InkFeature

This is the “flattened” tree, showing all the widgets projected through their various build functions. (This is the tree you obtain if you call toStringDeep() on the root of the widget tree.) You’ll see a lot of widgets in there that don’t appear in your application’s source, because they are inserted by the framework’s widgets’ build functions. For example, InkFeature is an implementation detail of the Material widget.

当按钮被点击响应时,debugDumpApp() 方法被调用,由于该方法与 TextButton 对象调用 setState() 相一致,因此 TextButton 对应的元素会被标记为 dirty。这就是为什么在查看转储信息时,您会看到被标记为「dirty」的特定对象。您也可以看到已经被注册的手势监听器;在这个案例中,列出了一个 GestureDetector,它只监听「tap」手势(这里「tap」是 TapGestureDetectortoStringShort 函数输出的)。

Since the debugDumpApp() call is invoked when the button changes from being pressed to being released, it coincides with the TextButton object calling setState() and thus marking itself dirty. That is why, when you look at the dump you should see that specific object marked as “dirty”. You can also see what gesture listeners have been registered; in this case, a single GestureDetector is listed, and it is listening only to a “tap” gesture (“tap” is the output of a TapGestureDetector’s toStringShort function).

对于您自定义的 widget,可以通过重写 debugFillProperties() 方法添加信息。为方法中的参数添加 DiagnosticsProperty 对象,并调用父类方法。该方法在 widget 调用 toString 方法时会被填充到其描述信息中。

If you write your own widgets, you can add information by overriding debugFillProperties(). Add DiagnosticsProperty objects to the method’s argument, and call the superclass method. This function is what the toString method uses to fill in the widget’s description.

Render 树

Render tree

如果您试图调试一个布局问题,那么 Widget 层的树可能不够详细。在这种情况下,您可以通过调用 debugDumpRenderTree() 转储 Render 树信息。和 debugDumpApp() 一样,除了在布局或绘制阶段,可以在任何时候调用它。一般来说,最好在 frame callback 或事件处理中调用它。

If you are trying to debug a layout issue, then the Widgets layer’s tree might be insufficiently detailed. In that case, you can dump the rendering tree by calling debugDumpRenderTree(). As with debugDumpApp(), you can call this more or less any time except during a layout or paint phase. As a general rule, calling it from a frame callback or an event handler is the best solution.

想要调用 debugDumpRenderTree() 方法,您需要在源码文件中添加 import 'package:flutter/rendering.dart';

To call debugDumpRenderTree(), you need to add import 'package:flutter/rendering.dart'; to your source file.

前面的小案例输出结构如下所示:

The output for the previous tiny example would look something like the following:

I/flutter ( 6559): RenderView
I/flutter ( 6559):  │ debug mode enabled - android
I/flutter ( 6559):  │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559):  │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559):  │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559):  │
I/flutter ( 6559):  └─child: RenderCustomPaint
I/flutter ( 6559):    │ creator: CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559):    │   WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559):    │   Theme ← AnimatedTheme ← ScrollConfiguration ← MaterialApp ←
I/flutter ( 6559):    │   [root]
I/flutter ( 6559):    │ parentData: <none>
I/flutter ( 6559):    │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):    │ size: Size(411.4, 683.4)
I/flutter ( 6559):    │
I/flutter ( 6559):    └─child: RenderPointerListener
I/flutter ( 6559):      │ creator: Listener ← Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):      │   _WidgetsAppState(552902158)] ← Title ← LocaleQuery ← MediaQuery
I/flutter ( 6559):      │   ← DefaultTextStyle ← CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559):      │   WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559):      │   Theme ← AnimatedTheme ← ⋯
I/flutter ( 6559):      │ parentData: <none>
I/flutter ( 6559):      │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):      │ size: Size(411.4, 683.4)
I/flutter ( 6559):      │ behavior: defer-to-child
I/flutter ( 6559):      │ listeners: down, up, cancel
I/flutter ( 6559):      │
I/flutter ( 6559):      └─child: RenderAbsorbPointer
I/flutter ( 6559):        │ creator: AbsorbPointer ← Listener ←
I/flutter ( 6559):        │   Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):        │   _WidgetsAppState(552902158)] ← Title ← LocaleQuery ← MediaQuery
I/flutter ( 6559):        │   ← DefaultTextStyle ← CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559):        │   WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559):        │   Theme ← ⋯
I/flutter ( 6559):        │ parentData: <none>
I/flutter ( 6559):        │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):        │ size: Size(411.4, 683.4)
I/flutter ( 6559):        │ absorbing: false
I/flutter ( 6559):        │
I/flutter ( 6559):        └─child: RenderSemanticsAnnotations
I/flutter ( 6559):          │ creator: Semantics ← Focus-[GlobalKey 489139594] ← AbsorbPointer
I/flutter ( 6559):          │   ← Listener ← Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):          │   _WidgetsAppState(552902158)] ← Title ← LocaleQuery ← MediaQuery
I/flutter ( 6559):          │   ← DefaultTextStyle ← CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559):          │   ⋯
I/flutter ( 6559):          │ parentData: <none>
I/flutter ( 6559):          │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):          │ size: Size(411.4, 683.4)
I/flutter ( 6559):          │
I/flutter ( 6559):          └─child: _RenderTheatre
I/flutter ( 6559):            │ creator: _Theatre ← Overlay-[GlobalKey 199833992] ← _FocusScope ←
I/flutter ( 6559):            │   Semantics ← Focus-[GlobalKey 489139594] ← AbsorbPointer ←
I/flutter ( 6559):            │   Listener ← Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):            │   _WidgetsAppState(552902158)] ← Title ← LocaleQuery ← MediaQuery
I/flutter ( 6559):            │   ← DefaultTextStyle ← ⋯
I/flutter ( 6559):            │ parentData: <none>
I/flutter ( 6559):            │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            │ size: Size(411.4, 683.4)
I/flutter ( 6559):            │
I/flutter ( 6559):            ├─onstage: RenderStack
I/flutter ( 6559):            ╎ │ creator: Stack ← _Theatre ← Overlay-[GlobalKey 199833992] ←
I/flutter ( 6559):            ╎ │   _FocusScope ← Semantics ← Focus-[GlobalKey 489139594] ←
I/flutter ( 6559):            ╎ │   AbsorbPointer ← Listener ←
I/flutter ( 6559):            ╎ │   Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):            ╎ │   _WidgetsAppState(552902158)] ← Title ← LocaleQuery ← MediaQuery
I/flutter ( 6559):            ╎ │   ← ⋯
I/flutter ( 6559):            ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0)
I/flutter ( 6559):            ╎ │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │
I/flutter ( 6559):            ╎ ├─child 1: RenderIgnorePointer
I/flutter ( 6559):            ╎ │ │ creator: IgnorePointer ← _OverlayEntry-[GlobalKey 612888877] ←
I/flutter ( 6559):            ╎ │ │   Stack ← _Theatre ← Overlay-[GlobalKey 199833992] ← _FocusScope
I/flutter ( 6559):            ╎ │ │   ← Semantics ← Focus-[GlobalKey 489139594] ← AbsorbPointer ←
I/flutter ( 6559):            ╎ │ │   Listener ← Navigator-[GlobalObjectKey<NavigatorState>
I/flutter ( 6559):            ╎ │ │   _WidgetsAppState(552902158)] ← Title ← ⋯
I/flutter ( 6559):            ╎ │ │ parentData: not positioned; offset=Offset(0.0, 0.0)
I/flutter ( 6559):            ╎ │ │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │ │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │ │ ignoring: false
I/flutter ( 6559):            ╎ │ │ ignoringSemantics: implicitly false
I/flutter ( 6559):            ╎ │ │
I/flutter ( 6559):            ╎ │ └─child: RenderSemanticsAnnotations
I/flutter ( 6559):            ╎ │   │ creator: Semantics ← ModalBarrier ← IgnorePointer ←
I/flutter ( 6559):            ╎ │   │   _OverlayEntry-[GlobalKey 612888877] ← Stack ← _Theatre ←
I/flutter ( 6559):            ╎ │   │   Overlay-[GlobalKey 199833992] ← _FocusScope ← Semantics ←
I/flutter ( 6559):            ╎ │   │   Focus-[GlobalKey 489139594] ← AbsorbPointer ← Listener ← ⋯
I/flutter ( 6559):            ╎ │   │ parentData: <none>
I/flutter ( 6559):            ╎ │   │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │   │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │   │
I/flutter ( 6559):            ╎ │   └─child: RenderSemanticsGestureHandler
I/flutter ( 6559):            ╎ │     │ creator: _GestureSemantics ← RawGestureDetector ← GestureDetector
I/flutter ( 6559):            ╎ │     │   ← Semantics ← ModalBarrier ← IgnorePointer ←
I/flutter ( 6559):            ╎ │     │   _OverlayEntry-[GlobalKey 612888877] ← Stack ← _Theatre ←
I/flutter ( 6559):            ╎ │     │   Overlay-[GlobalKey 199833992] ← _FocusScope ← Semantics ← ⋯
I/flutter ( 6559):            ╎ │     │ parentData: <none>
I/flutter ( 6559):            ╎ │     │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │     │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │     │
I/flutter ( 6559):            ╎ │     └─child: RenderPointerListener
I/flutter ( 6559):            ╎ │       │ creator: Listener ← _GestureSemantics ← RawGestureDetector ←
I/flutter ( 6559):            ╎ │       │   GestureDetector ← Semantics ← ModalBarrier ← IgnorePointer ←
I/flutter ( 6559):            ╎ │       │   _OverlayEntry-[GlobalKey 612888877] ← Stack ← _Theatre ←
I/flutter ( 6559):            ╎ │       │   Overlay-[GlobalKey 199833992] ← _FocusScope ← ⋯
I/flutter ( 6559):            ╎ │       │ parentData: <none>
I/flutter ( 6559):            ╎ │       │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │       │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │       │ behavior: opaque
I/flutter ( 6559):            ╎ │       │ listeners: down
I/flutter ( 6559):            ╎ │       │
I/flutter ( 6559):            ╎ │       └─child: RenderConstrainedBox
I/flutter ( 6559):            ╎ │           creator: ConstrainedBox ← Listener ← _GestureSemantics ←
I/flutter ( 6559):            ╎ │             RawGestureDetector ← GestureDetector ← Semantics ← ModalBarrier
I/flutter ( 6559):            ╎ │             ← IgnorePointer ← _OverlayEntry-[GlobalKey 612888877] ← Stack ←
I/flutter ( 6559):            ╎ │             _Theatre ← Overlay-[GlobalKey 199833992] ← ⋯
I/flutter ( 6559):            ╎ │           parentData: <none>
I/flutter ( 6559):            ╎ │           constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎ │           size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎ │           additionalConstraints: BoxConstraints(biggest)
I/flutter ( 6559):            ╎ │
I/flutter ( 6559):            ╎ └─child 2: RenderSemanticsAnnotations
I/flutter ( 6559):            ╎   │ creator: Semantics ← Focus-[GlobalObjectKey
I/flutter ( 6559):            ╎   │   MaterialPageRoute<void>(875520219)] ← _ModalScope-[GlobalKey
I/flutter ( 6559):            ╎   │   816151164] ← _OverlayEntry-[GlobalKey 727622716] ← Stack ←
I/flutter ( 6559):            ╎   │   _Theatre ← Overlay-[GlobalKey 199833992] ← _FocusScope ←
I/flutter ( 6559):            ╎   │   Semantics ← Focus-[GlobalKey 489139594] ← AbsorbPointer ←
I/flutter ( 6559):            ╎   │   Listener ← ⋯
I/flutter ( 6559):            ╎   │ parentData: not positioned; offset=Offset(0.0, 0.0)
I/flutter ( 6559):            ╎   │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎   │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎   │
I/flutter ( 6559):            ╎   └─child: RenderOffstage
I/flutter ( 6559):            ╎     │ creator: Offstage ← _FocusScope ← Semantics ←
I/flutter ( 6559):            ╎     │   Focus-[GlobalObjectKey MaterialPageRoute<void>(875520219)] ←
I/flutter ( 6559):            ╎     │   _ModalScope-[GlobalKey 816151164] ← _OverlayEntry-[GlobalKey
I/flutter ( 6559):            ╎     │   727622716] ← Stack ← _Theatre ← Overlay-[GlobalKey 199833992] ←
I/flutter ( 6559):            ╎     │   _FocusScope ← Semantics ← Focus-[GlobalKey 489139594] ← ⋯
I/flutter ( 6559):            ╎     │ parentData: <none>
I/flutter ( 6559):            ╎     │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎     │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎     │ offstage: false
I/flutter ( 6559):            ╎     │
I/flutter ( 6559):            ╎     └─child: RenderIgnorePointer
I/flutter ( 6559):            ╎       │ creator: IgnorePointer ← Offstage ← _FocusScope ← Semantics ←
I/flutter ( 6559):            ╎       │   Focus-[GlobalObjectKey MaterialPageRoute<void>(875520219)] ←
I/flutter ( 6559):            ╎       │   _ModalScope-[GlobalKey 816151164] ← _OverlayEntry-[GlobalKey
I/flutter ( 6559):            ╎       │   727622716] ← Stack ← _Theatre ← Overlay-[GlobalKey 199833992] ←
I/flutter ( 6559):            ╎       │   _FocusScope ← Semantics ← ⋯
I/flutter ( 6559):            ╎       │ parentData: <none>
I/flutter ( 6559):            ╎       │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎       │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎       │ ignoring: false
I/flutter ( 6559):            ╎       │ ignoringSemantics: implicitly false
I/flutter ( 6559):            ╎       │
I/flutter ( 6559):            ╎       └─child: RenderFractionalTranslation
I/flutter ( 6559):            ╎         │ creator: FractionalTranslation ← SlideTransition ←
I/flutter ( 6559):            ╎         │   _MountainViewPageTransition ← IgnorePointer ← Offstage ←
I/flutter ( 6559):            ╎         │   _FocusScope ← Semantics ← Focus-[GlobalObjectKey
I/flutter ( 6559):            ╎         │   MaterialPageRoute<void>(875520219)] ← _ModalScope-[GlobalKey
I/flutter ( 6559):            ╎         │   816151164] ← _OverlayEntry-[GlobalKey 727622716] ← Stack ←
I/flutter ( 6559):            ╎         │   _Theatre ← ⋯
I/flutter ( 6559):            ╎         │ parentData: <none>
I/flutter ( 6559):            ╎         │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎         │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎         │ translation: Offset(0.0, 0.0)
I/flutter ( 6559):            ╎         │ transformHitTests: true
I/flutter ( 6559):            ╎         │
I/flutter ( 6559):            ╎         └─child: RenderRepaintBoundary
I/flutter ( 6559):            ╎           │ creator: RepaintBoundary ← FractionalTranslation ←
I/flutter ( 6559):            ╎           │   SlideTransition ← _MountainViewPageTransition ← IgnorePointer ←
I/flutter ( 6559):            ╎           │   Offstage ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey
I/flutter ( 6559):            ╎           │   MaterialPageRoute<void>(875520219)] ← _ModalScope-[GlobalKey
I/flutter ( 6559):            ╎           │   816151164] ← _OverlayEntry-[GlobalKey 727622716] ← Stack ← ⋯
I/flutter ( 6559):            ╎           │ parentData: <none>
I/flutter ( 6559):            ╎           │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎           │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎           │ metrics: 83.3% useful (1 bad vs 5 good)
I/flutter ( 6559):            ╎           │ diagnosis: this is a useful repaint boundary and should be kept
I/flutter ( 6559):            ╎           │
I/flutter ( 6559):            ╎           └─child: RenderDecoratedBox
I/flutter ( 6559):            ╎             │ creator: DecoratedBox ← Container ← AnimatedContainer ← Material
I/flutter ( 6559):            ╎             │   ← AppHome ← _ModalScopeStatus ← PageStorage-[GlobalKey
I/flutter ( 6559):            ╎             │   619728754] ← RepaintBoundary ← FractionalTranslation ←
I/flutter ( 6559):            ╎             │   SlideTransition ← _MountainViewPageTransition ← IgnorePointer ←
I/flutter ( 6559):            ╎             │   ⋯
I/flutter ( 6559):            ╎             │ parentData: <none>
I/flutter ( 6559):            ╎             │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎             │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎             │ decoration:
I/flutter ( 6559):            ╎             │   <no decorations specified>
I/flutter ( 6559):            ╎             │ configuration: ImageConfiguration(bundle:
I/flutter ( 6559):            ╎             │   PlatformAssetBundle@367106502(), devicePixelRatio: 2.625,
I/flutter ( 6559):            ╎             │   platform: android)
I/flutter ( 6559):            ╎             │
I/flutter ( 6559):            ╎             └─child: RenderDecoratedBox
I/flutter ( 6559):            ╎               │ creator: DecoratedBox ← Container ← DecoratedBox ← Container ←
I/flutter ( 6559):            ╎               │   AnimatedContainer ← Material ← AppHome ← _ModalScopeStatus ←
I/flutter ( 6559):            ╎               │   PageStorage-[GlobalKey 619728754] ← RepaintBoundary ←
I/flutter ( 6559):            ╎               │   FractionalTranslation ← SlideTransition ← ⋯
I/flutter ( 6559):            ╎               │ parentData: <none>
I/flutter ( 6559):            ╎               │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎               │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎               │ decoration:
I/flutter ( 6559):            ╎               │   backgroundColor: Color(0xfffafafa)
I/flutter ( 6559):            ╎               │ configuration: ImageConfiguration(bundle:
I/flutter ( 6559):            ╎               │   PlatformAssetBundle@367106502(), devicePixelRatio: 2.625,
I/flutter ( 6559):            ╎               │   platform: android)
I/flutter ( 6559):            ╎               │
I/flutter ( 6559):            ╎               └─child: _RenderInkFeatures
I/flutter ( 6559):            ╎                 │ creator: _InkFeature-[GlobalKey ink renderer] ←
I/flutter ( 6559):            ╎                 │   NotificationListener<LayoutChangedNotification> ← DecoratedBox
I/flutter ( 6559):            ╎                 │   ← Container ← DecoratedBox ← Container ← AnimatedContainer ←
I/flutter ( 6559):            ╎                 │   Material ← AppHome ← _ModalScopeStatus ← PageStorage-[GlobalKey
I/flutter ( 6559):            ╎                 │   619728754] ← RepaintBoundary ← ⋯
I/flutter ( 6559):            ╎                 │ parentData: <none>
I/flutter ( 6559):            ╎                 │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎                 │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎                 │
I/flutter ( 6559):            ╎                 └─child: RenderPositionedBox
I/flutter ( 6559):            ╎                   │ creator: Center ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
I/flutter ( 6559):            ╎                   │   _InkFeature-[GlobalKey ink renderer] ←
I/flutter ( 6559):            ╎                   │   NotificationListener<LayoutChangedNotification> ← DecoratedBox
I/flutter ( 6559):            ╎                   │   ← Container ← DecoratedBox ← Container ← AnimatedContainer ←
I/flutter ( 6559):            ╎                   │   Material ← AppHome ← ⋯
I/flutter ( 6559):            ╎                   │ parentData: <none>
I/flutter ( 6559):            ╎                   │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):            ╎                   │ size: Size(411.4, 683.4)
I/flutter ( 6559):            ╎                   │ alignment: Alignment.center
I/flutter ( 6559):            ╎                   │ widthFactor: expand
I/flutter ( 6559):            ╎                   │ heightFactor: expand
I/flutter ( 6559):            ╎                   │
I/flutter ( 6559):            ╎                   └─child: RenderConstrainedBox relayoutBoundary=up1
I/flutter ( 6559):            ╎                     │ creator: ConstrainedBox ← MaterialButton ← TextButton ← Center ←
I/flutter ( 6559):            ╎                     │   DefaultTextStyle ← AnimatedDefaultTextStyle ←
I/flutter ( 6559):            ╎                     │   _InkFeature-[GlobalKey ink renderer] ←
I/flutter ( 6559):            ╎                     │   NotificationListener<LayoutChangedNotification> ← DecoratedBox
I/flutter ( 6559):            ╎                     │   ← Container ← DecoratedBox ← Container ← ⋯
I/flutter ( 6559):            ╎                     │ parentData: offset=Offset(156.7, 323.7)
I/flutter ( 6559):            ╎                     │ constraints: BoxConstraints(0.0<=w<=411.4, 0.0<=h<=683.4)
I/flutter ( 6559):            ╎                     │ size: Size(98.0, 36.0)
I/flutter ( 6559):            ╎                     │ additionalConstraints: BoxConstraints(88.0<=w<=Infinity, h=36.0)
I/flutter ( 6559):            ╎                     │
I/flutter ( 6559):            ╎                     └─child: RenderSemanticsGestureHandler relayoutBoundary=up2
I/flutter ( 6559):            ╎                       │ creator: _GestureSemantics ← RawGestureDetector ← GestureDetector
I/flutter ( 6559):            ╎                       │   ← InkWell ← IconTheme ← DefaultTextStyle ←
I/flutter ( 6559):            ╎                       │   AnimatedDefaultTextStyle ← ConstrainedBox ← MaterialButton ←
I/flutter ( 6559):            ╎                       │   TextButton ← Center ← DefaultTextStyle ← ⋯
I/flutter ( 6559):            ╎                       │ parentData: <none>
I/flutter ( 6559):            ╎                       │ constraints: BoxConstraints(88.0<=w<=411.4, h=36.0)
I/flutter ( 6559):            ╎                       │ size: Size(98.0, 36.0)
I/flutter ( 6559):            ╎                       │
I/flutter ( 6559):            ╎                       └─child: RenderPointerListener relayoutBoundary=up3
I/flutter ( 6559):            ╎                         │ creator: Listener ← _GestureSemantics ← RawGestureDetector ←
I/flutter ( 6559):            ╎                         │   GestureDetector ← InkWell ← IconTheme ← DefaultTextStyle ←
I/flutter ( 6559):            ╎                         │   AnimatedDefaultTextStyle ← ConstrainedBox ← MaterialButton ←
I/flutter ( 6559):            ╎                         │   TextButton ← Center ← ⋯
I/flutter ( 6559):            ╎                         │ parentData: <none>
I/flutter ( 6559):            ╎                         │ constraints: BoxConstraints(88.0<=w<=411.4, h=36.0)
I/flutter ( 6559):            ╎                         │ size: Size(98.0, 36.0)
I/flutter ( 6559):            ╎                         │ behavior: opaque
I/flutter ( 6559):            ╎                         │ listeners: down
I/flutter ( 6559):            ╎                         │
I/flutter ( 6559):            ╎                         └─child: RenderPadding relayoutBoundary=up4
I/flutter ( 6559):            ╎                           │ creator: Padding ← Container ← Listener ← _GestureSemantics ←
I/flutter ( 6559):            ╎                           │   RawGestureDetector ← GestureDetector ← InkWell ← IconTheme ←
I/flutter ( 6559):            ╎                           │   DefaultTextStyle ← AnimatedDefaultTextStyle ← ConstrainedBox ←
I/flutter ( 6559):            ╎                           │   MaterialButton ← ⋯
I/flutter ( 6559):            ╎                           │ parentData: <none>
I/flutter ( 6559):            ╎                           │ constraints: BoxConstraints(88.0<=w<=411.4, h=36.0)
I/flutter ( 6559):            ╎                           │ size: Size(98.0, 36.0)
I/flutter ( 6559):            ╎                           │ padding: EdgeInsets(16.0, 0.0, 16.0, 0.0)
I/flutter ( 6559):            ╎                           │
I/flutter ( 6559):            ╎                           └─child: RenderPositionedBox relayoutBoundary=up5
I/flutter ( 6559):            ╎                             │ creator: Center ← Padding ← Container ← Listener ←
I/flutter ( 6559):            ╎                             │   _GestureSemantics ← RawGestureDetector ← GestureDetector ←
I/flutter ( 6559):            ╎                             │   InkWell ← IconTheme ← DefaultTextStyle ←
I/flutter ( 6559):            ╎                             │   AnimatedDefaultTextStyle ← ConstrainedBox ← ⋯
I/flutter ( 6559):            ╎                             │ parentData: offset=Offset(16.0, 0.0)
I/flutter ( 6559):            ╎                             │ constraints: BoxConstraints(56.0<=w<=379.4, h=36.0)
I/flutter ( 6559):            ╎                             │ size: Size(66.0, 36.0)
I/flutter ( 6559):            ╎                             │ alignment: Alignment.center
I/flutter ( 6559):            ╎                             │ widthFactor: 1.0
I/flutter ( 6559):            ╎                             │ heightFactor: expand
I/flutter ( 6559):            ╎                             │
I/flutter ( 6559):            ╎                             └─child: RenderParagraph relayoutBoundary=up6
I/flutter ( 6559):            ╎                               │ creator: RichText ← Text ← Center ← Padding ← Container ←
I/flutter ( 6559):            ╎                               │   Listener ← _GestureSemantics ← RawGestureDetector ←
I/flutter ( 6559):            ╎                               │   GestureDetector ← InkWell ← IconTheme ← DefaultTextStyle ← ⋯
I/flutter ( 6559):            ╎                               │ parentData: offset=Offset(0.0, 10.0)
I/flutter ( 6559):            ╎                               │ constraints: BoxConstraints(0.0<=w<=379.4, 0.0<=h<=36.0)
I/flutter ( 6559):            ╎                               │ size: Size(66.0, 16.0)
I/flutter ( 6559):            ╎                               ╘═╦══ text ═══
I/flutter ( 6559):            ╎                                 ║ TextSpan:
I/flutter ( 6559):            ╎                                 ║   inherit: false
I/flutter ( 6559):            ╎                                 ║   color: Color(0xdd000000)
I/flutter ( 6559):            ╎                                 ║   family: "Roboto"
I/flutter ( 6559):            ╎                                 ║   size: 14.0
I/flutter ( 6559):            ╎                                 ║   weight: 500
I/flutter ( 6559):            ╎                                 ║   baseline: alphabetic
I/flutter ( 6559):            ╎                                 ║   "Dump App"
I/flutter ( 6559):            ╎                                 ╚═══════════
I/flutter ( 6559):            ╎
I/flutter ( 6559):            └╌no offstage children

这是根节点 RenderObject 对象的 toStringDeep() 方法的输出结果。

This is the output of the root RenderObject object’s toStringDeep() function.

在调试布局问题时,主要需要关注 sizeconstraints 两个字段。 constraint 沿树向下传递,而 size 则向上追溯。

When debugging layout issues, the key fields to look at are the size and constraints fields. The constraints flow down the tree, and the sizes flow back up.

比如,从上面转储信息中可以看出窗口尺寸是 Size(411.4, 683.4),它用于强制 RenderPositionedBox 之前的所有 box 为屏幕尺寸,其约束为 BoxConstraints(w=411.4, h=683.4)。从转储文件可以看出 RenderPositionedBox 是由 Center widget 创建的(可以从 creator 字段的描述看出来),并将其 child 的约束条件变得松散:约束范围是 BoxConstraints(0.0<=w<=411.4, 0.0<=h<=683.4)。其后代的 RenderPadding 进一步插入这些约束来确保留出空间作为内边距,因此 RenderConstrainedBox 有一个宽松的约束,该约束为: BoxConstraints(0.0<=w<=395.4,0.0<=h<=667.4)creator 字段告诉我们,这个对象很可能是 TextButton 定义的一部分,它内容的最小宽度为 88 像素,具体高度为 36.0。( TextButton 是 Material Design 中按钮尺寸标准的实现。)

For example, in the previous dump you can see that the window size, Size(411.4, 683.4), is used to force all the boxes down to the RenderPositionedBox to be the size of the screen, with constraints of BoxConstraints(w=411.4, h=683.4). The RenderPositionedBox, which the dump says was created by a Center widget (as described by the creator field), sets its child’s constraints to a loose version of this: BoxConstraints(0.0<=w<=411.4, 0.0<=h<=683.4). The child, a RenderPadding, further inserts these constraints to ensure there is room for the padding, and thus the RenderConstrainedBox has a loose constraint of BoxConstraints(0.0<=w<=395.4, 0.0<=h<=667.4). This object, which the creator field tells us is probably part of the TextButton’s definition, sets a minimum width of 88 pixels on its contents and a specific height of 36.0. (This is the TextButton class implementing the Material Design guidelines regarding button dimensions.)

最内部的 RenderPositionedBox 再次放松了约束,这次是把文本放在了按钮的中间。 RenderParagraph 可以根据其内容确定自身大小。如果您现在沿着这条链路往回追溯渲染对象的尺寸大小,您就会看到在文本的大小是如何影响按钮边框大小的形成过程,因为它们都会根据子组件的尺寸自行调整大小。

The inner-most RenderPositionedBox loosens the constraints again, this time to center the text within the button. The RenderParagraph picks its size based on its contents. If you now follow the sizes back up the chain, you’ll see how the text’s size is what influences the width of all the boxes that form the button, as they all take their child’s dimensions to size themselves.

注意到这点的另一种方式为:查看每个 box 的「relayoutSubtreeRoot」部分,它本质上在告诉您,在某种程度上有多少祖先在依赖于这个元素的尺寸。因此,RenderParagraphrelayoutSubtreeRoot=up8,这意味着当 RenderParagraph 被标为 dirty 时,8 个祖先也会被标为 dirty,因为它们可能会受到新尺寸的影响。

Another way to notice this is by looking at the “relayoutSubtreeRoot” part of the descriptions of each box, which essentially tells you how many ancestors depend on this element’s size in some way. Thus the RenderParagraph has a relayoutSubtreeRoot=up8, meaning that when the RenderParagraph is dirtied, eight ancestors also have to be dirtied because they might be affected by the new dimensions.

对于您自己写的 render 对象,可以通过重写 debugFillProperties() 方法为转储数据添加信息。在方法中的参数中添加 DiagnosticsProperty 对象,并调用父类方法即可。

If you write your own render objects, you can add information to the dump by overriding debugFillProperties(). Add DiagnosticsProperty objects to the method’s argument, and call the superclass method.

Layer 树

Layer tree

如果您在尝试调试一个合成问题,您可以使用 debugDumpLayerTree()。在前面案例中调用这个方法,会输出如下结果:

If you are trying to debug a compositing issue, you can use debugDumpLayerTree(). For the previous example, it would output:

I/flutter : TransformLayer
I/flutter :  │ creator: [root]
I/flutter :  │ offset: Offset(0.0, 0.0)
I/flutter :  │ transform:
I/flutter :  │   [0] 3.5,0.0,0.0,0.0
I/flutter :  │   [1] 0.0,3.5,0.0,0.0
I/flutter :  │   [2] 0.0,0.0,1.0,0.0
I/flutter :  │   [3] 0.0,0.0,0.0,1.0
I/flutter :  │
I/flutter :  ├─child 1: OffsetLayer
I/flutter :  │ │ creator: RepaintBoundary ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355] ← Stack ← Overlay-[GlobalKey 625702218] ← Navigator-[GlobalObjectKey _MaterialAppState(859106034)] ← Title ← ⋯
I/flutter :  │ │ offset: Offset(0.0, 0.0)
I/flutter :  │ │
I/flutter :  │ └─child 1: PictureLayer
I/flutter :  │
I/flutter :  └─child 2: PictureLayer

这是根 Layer 对象调用 toStringDeep 方法时的输出结果。

This is the output of calling toStringDeep on the root Layer object.

根结点的 transform 是设备像素比率的变换;在该示例中,每个逻辑像素对应 3.5 个设备像素。

The transform at the root is the transform that applies the device pixel ratio; in this case, a ratio of 3.5 device pixels for every logical pixel.

RepaintBoundary widget 在 render 树中创建了一个 RenderRepaintBoundary,并在 layer 树中创建了一个新的层。这可以用来减少需要重绘的次数。

The RepaintBoundary widget, which creates a RenderRepaintBoundary in the render tree, creates a new layer in the layer tree. This is used to reduce how much needs to be repainted.

Semantics 树

Semantics tree

您也可以使用 debugDumpSemanticsTree() 获得 Semantics 树(该树提供了系统的 accessibility API)的转储信息。想要使用它,首先必须启用 accessibility,例如,通过启用系统 accessibility 工具或 SemanticsDebugger

You can also obtain a dump of the Semantics tree (the tree presented to the system accessibility APIs) using debugDumpSemanticsTree(). To use this, you have to have first enable accessibility, for example, by enabling a system accessibility tool or the SemanticsDebugger.

在前面案例中调用这个方法,会输出如下结果:

For the previous example, it would output the following:

I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :  ├SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :  │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter :  └SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :    └SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")

Scheduling

如果您想要找到事件触发对应的开始或结束帧,可以将 debugPrintBeginFrameBannerdebugPrintEndFrameBanner 这两个布尔值切换为 true,在控制台中打印开始和结束帧的信息。

To find out where your events happen relative to the frame’s begin/end, you can toggle the debugPrintBeginFrameBanner and the debugPrintEndFrameBanner booleans to print the beginning and end of the frames to the console.

比如:

For example:

I/flutter : ▄▄▄▄▄▄▄▄ Frame 12         30s 437.086ms ▄▄▄▄▄▄▄▄
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

当前帧被调度时,debugPrintScheduleFrameStacks 标志也可以用来打印调用堆栈信息。

The debugPrintScheduleFrameStacks flag can also be used to print the call stack causing the current frame to be scheduled.

调试标志:布局

Debug flags: layout

通过将 debugPaintSizeEnabled 设置为 true,您也可以可视地调试布局问题。该布尔值在 rendering 库中,可以在任何时候被启用,并且当其为 true 时,会影响界面上所有的绘制。最简单的方式是在程序顶部入口 void main()中设置它,如下案例代码所示:

You can also debug a layout problem visually, by setting debugPaintSizeEnabled to true. This is a boolean from the rendering library. It can be enabled at any time and affects all painting while it is true. The easiest way to set it is at the top of your void main() entry point. See an example in the following code:

//add import to rendering library
import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled=true;
  runApp(MyApp());
}

当它被启用时,所有的 box 都会有明亮的蓝绿色边框,内边距(来自于 widgets,比如 Padding)显示为淡蓝色,并在 child 周围有一个深蓝色的 box,对齐方式(来自于 widgets,比如 CenterAlign)显示为黄色箭头,还有间隔(来自于 widgets,比如当 Container 没有 child 时)显示灰色。

When it is enabled, all boxes get a bright teal border, padding (from widgets like Padding) is shown in faded blue with a darker blue box around the child, alignment (from widgets like Center and Align) is shown with yellow arrows, and spacers (from widgets like Container when they have no child) are shown in gray.

debugPaintBaselinesEnabled][] 标志和它类似,但只针对于带有基线的对象。 alphabetic 基线用亮绿色显示,ideographic 基线用橙色显示。

The debugPaintBaselinesEnabled flag does something similar but for objects with baselines. The alphabetic baseline is shown in bright green and the ideographic baseline in orange.

debugPaintPointersEnabled 标志会打开一个特殊模式,任何被选中的对象都会以蓝绿色高亮显示。这可以帮助您确定对象是否会以某种方式未能正确命中测试(这是可能会发生的,例如,实际上它在父节点的边界之外,因此一开始就不用考虑进行命中测试)。

The debugPaintPointersEnabled flag turns on a special mode whereby any objects that are being tapped get highlighted in teal. This can help you determine whether an object is somehow failing to correctly hit test (which might happen if, for instance, it is actually outside the bounds of its parent and thus not being considered for hit testing in the first place).

如果您试图调试合成层,比如要确定是否应该在某处添加 RepaintBoundary widget,您可以使用 debugPaintLayerBordersEnabled 标志,来用为每个 layer 的边界显示橙色边框,或使用 debugRepaintRainbowEnabled 标志,这会使得每当重新绘制图层时,边框的颜色就会被一组轮转的颜色覆盖。

If you’re trying to debug compositor layers, for example to determine whether and where to add RepaintBoundary widgets, you can use the debugPaintLayerBordersEnabled flag, which outlines each layer’s bounds in orange, or the debugRepaintRainbowEnabled flag, which causes layers to be overlayed with a rotating set of colors whenever they are repainted.

上面所有的标志都只在 调试模式 下生效。一般来说,Flutter 框架中以「debug...」开头的都只能在调试模式下工作。

All of these flags only work in debug mode. In general, anything in the Flutter framework that starts with “debug...” only works in debug mode.

调试动画

Debugging animations

timeDilation 变量(来自 scheduler 库)设置为大于 1.0 的数字,例如,50.0。该操作最好在应用启动时只执行一次。如果您动态地改变,尤其是在动画运行时减少它时,框架可能会观察到时间倒退,这可能会导致断言失败,通常这会让您徒劳无功。

Set the timeDilation variable (from the scheduler library) to a number greater than 1.0, for instance, 50.0. It’s best to only set this once on app startup. If you change it on the fly, especially if you reduce it while animations are running, it’s possible that the framework will observe time going backwards, which will probably result in asserts and generally interfere with your efforts.

调试标志:性能

Debug flags: performance

Flutter 提供了各种各样的调试标志和功能,来帮助您在开发周期的不同阶段调试应用。想要使用这些特性,必须在调试模式下编译。下面的列表虽然不完整,但是突出显示了 rendering library 中用于调试性能问题的一些标志(以及一个函数)。

Flutter provides a wide variety of debug flags and functions to help you debug your app at various points along the development cycle. To use these features, you must compile in debug mode. The following list, while not complete, highlights some of flags (and one function) from the rendering library for debugging performance issues.

您可以通过修改框架的代码来设置这些标志,或者将模块导入,并在 main() 方法中设置标志值,然后热重启。

You can set these flags either by editing the framework code, or by importing the module and setting the value in your main() method, following by a hot restart.

debugDumpRenderTree()

当不在布局或重新绘制阶段时,调用此函数将 render 树转储到控制台。(可以从 flutter run 按下 t 调用此命令。)通过搜索其中的「RepaintBoundary」可以查看关于边界的有用诊断信息。

Call this function when not in a layout or repaint phase to dump the rendering tree to the console. (Pressing t from flutter run calls this command.) Search for “RepaintBoundary” to see diagnostics on how useful a boundary is.

debugPaintLayerBordersEnabled

PENDING

debugRepaintRainbowEnabled

您可以通过点击 Highlight Repaints 按钮,在 Flutter inspector 中启用此标志。如果任何静态 widget 在彩虹七颜色之间轮转(比如一个静态标题),那么这些区域就可能需要添加重新绘制边界进行优化。

You can enable this flag in the Flutter inspector by selecting the Highlight Repaints button. If any static widgets are rotating through the colors of the rainbow (for example, a static header), those areas are candidates for adding repaint boundaries.

debugPrintMarkNeedsLayoutStacks

如果您看到的布局比预期的要多(比如,在 timeline 、profile 或者一个布局方法中的 print 语句中),可以启用这个标志。一旦启用,控制台将会充满堆栈跟踪,来显示在布局时每个渲染对象被标记为 dirty 的原因。如果有需要的话,您可以使用 services 库中的 debugPrintStack() 方法按需打印出堆栈的跟踪信息。

Enable this flag if you’re seeing more layouts than you expect (for example, on the timeline, on a profile, or from a print statement inside a layout method). Once enabled, the console is flooded with stack traces showing why each render object is being marked dirty for layout. You can use the debugPrintStack() method from the services library to print your own stack traces on demand, if this kind of approach is useful to you.

debugPrintMarkNeedsPaintStacks

它和 debugPrintMarkNeedsLayoutStacks 类似,但用于多余的绘制。如果有需要的话,您可以使用 services 库中的 debugPrintStack() 方法按需打印出堆栈的跟踪信息。

Similar to debugPrintMarkNeedsLayoutStacks, but for excess painting. You can use the debugPrintStack() method from the services library to print your own stack traces on demand, if this kind of approach is useful to you.

跟踪 Dart 代码性能

Tracing Dart code performance

想要以编程方式执行自定义性能跟踪和测量任意代码片段的 wall/CPU 时间,这类似于在 Android 上使用 systrace,您可以使用 dart:developer 包中的 Timeline 类提供的一些静态方法包裹您想测量的代码,比如:

To perform custom performance traces programmatically and measure wall/CPU time of arbitrary segments of Dart code similar to what would be done on Android with systrace, use dart:developer Timeline utilities to wrap the code you want to measure such as:

import 'dart:developer';

Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();

然后打开您应用观测台的 timeline 页面,勾选 “Dart” 记录选项,并执行您想要测量的方法。

Then open your app’s Observatory’s timeline page, check the ‘Dart’ recording option and perform the function you want to measure.

在 Chrome 的 跟踪工具 中刷新页面,展示您应用的 timeline 时序记录。

Refreshing the page displays the chronological timeline records of your app in Chrome’s tracing tool.

确保以 性能模式 运行您的应用,来确保运行时的性能表现与您的最终产品相近。

Be sure to run your app in profile mode to ensure that the runtime performance characteristics closely match that of your final product.

性能图层

Performance overlay

您可以通过编程方式启用 PerformanceOverlay widget,在 MaterialAppCupertinoAppWidgetsApp 构造函数中,将 showPerformanceOverlay 属性设置为 true 即可。

You can programmatically enable the PerformanceOverlay widget by setting the showPerformanceOverlay property to true on the MaterialApp, CupertinoApp, or WidgetsApp constructor:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showPerformanceOverlay: true,
      title: 'My Awesome App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'My Awesome App'),
    );
  }
}

(如果您没有使用 MaterialAppCupertinoAppWidgetsApp,可以通过将应用包装在一个 Stack 中,并通过调用 PerformanceOverlay.allEnabled() 来创建一个 widget,来获得相同的效果。)

(If you’re not using MaterialApp, CupertinoApp, or WidgetsApp, you can get the same effect by wrapping your application in a stack and putting a widget on your stack that was created by calling PerformanceOverlay.allEnabled().)

有关如何解释浮层中的图形的信息,可以参见 Flutter 性能分析 中的 性能图层

For information on how to interpret the graphs in the overlay, see The performance overlay in Profiling Flutter performance.

Widget 对齐网格

Widget alignment grid

您可以通过编程的方式将 Material Design 基线网格 覆盖在应用的顶层来辅助对齐校验,通过使用 MaterialApp 构造函数 中的 debugShowMaterialGrid 参数进行设置。

You can programmatically overlay a Material Design baseline grid on top of your app to help verify alignments by using the debugShowMaterialGrid argument in the MaterialApp constructor.

在非 Material 应用中,您可以通过直接使用 GridPaper widget 来达到类似的效果。

In non-Material applications, you can achieve a similar effect by using a GridPaper widget directly.

使用原生的调试器

目录

如果你只使用 Dart 语言开发 Flutter 应用,并且不使用特定于平台的的库或者功能,你可以使用 IDE 的调试器调试你的代码。只有这篇指南的第一部分「调试 Dart 代码」对你有用。

If you are exclusively writing Flutter apps with Dart code and not using platform-specific libraries, or otherwise accessing platform-specific features, you can debug your code using your IDE’s debugger. Only the first section of this guide, Debugging Dart code, is relevant for you.

如果你正在开发特定于平台的的插件或者使用由 Swift、ObjectiveC、Java 或 Kotlin 语言编写的特定于平台的库,你可以使用 Xcode(用于 iOS)或者 Android Gradle(用于 Android)调试这部分代码。本指南介绍如何将用于 Dart 和用于原生代码的 两个 调试器连接到你的 Dart 应用。

If you’re writing a platform-specific plugin or using platform-specific libraries written in Swift, ObjectiveC, Java, or Kotlin, you can debug that portion of your code using Xcode (for iOS) or Android Gradle (for Android). This guide shows you how you can connect two debuggers to your Dart app, one for Dart, and one for the OEM code.

调试 Dart 代码

Debugging Dart code

你可以使用 IDE 进行一般的 Dart 调试。以下内容针对 Android Studio 进行说明,但你也可以使用你喜欢的安装并配置好 Flutter 和 Dart 插件的编辑器来进行调试。

Use your IDE for standard Dart debugging. These instructions describe Android Studio, but you can use your preferred IDE with the Flutter and Dart plugins installed and configured.

Dart 调试器

Dart debugger

你可以 step in/out/over Dart 语句、热重载和恢复执行应用、以及像使用其他调试器一样来使用 Dart 调试器。 5: Debug 按钮切换调试面板的显示。

You can step in, out, and over Dart statements, hot reload or resume the app, and use the debugger in the same way you’d use any debugger. The 5: Debug button toggles display of the debug pane.

Flutter inspector

Flutter 插件提供了另外两个可能给你提供帮助的功能。 Flutter inspector 是一个用来可视化以及查看 Flutter widget 树的工具,并帮助你:

There are two other features provided by the Flutter plugin that you might find useful. The Flutter inspector is a tool for visualizing and exploring the Flutter widget tree and helps you:

你可以使用 Android Studio 窗口右侧的垂直按钮切换检查器的显示。

Toggle display of the inspector using the vertical button to the right of the Android Studio window.

Flutter inspector

Flutter outline

Flutter Outline 以可视形式显示构建方法。注意在构建方法上可能与 widget 树不同。你可以使用 Android Studio 窗口右侧的垂直按钮切换 outline 的显示。

The Flutter Outline displays the build method in visual form. Note that this might be different than the widget tree for the build method. Toggle display of the outline using the vertical button to the right of the AS window.

screenshot showing the Flutter inspector

这篇指南剩下的部分介绍了如何搭建原生代码的调试环境。你应该可以想象到,对于 iOS 和 Android 这个过程是不同的。

The rest of this guide shows how to set up your environment to debug OEM code. As you’d expect, the process works differently for iOS and Android.

使用 Android Gradle 调试(Android)

Debugging with Android Gradle (Android)

为了调试原生代码,你需要一个包含 Android 原生代码的应用。在本节中,你将学会如何连接两个调试器到你的应用: 1)Dart 调试器,和 2)Android Gradle 调试器。

In order to debug OEM Android code, you need an app that contains OEM Android code. In this section, you’ll learn how to connect two debuggers to your app: 1) the Dart debugger and, 2) the Android Gradle debugger.

// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'URL Launcher',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'URL Launcher'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<void> _launched;

  Future<void> _launchInBrowser(String url) async {
    if (await canLaunch(url)) {
      await launch(url, forceSafariVC: false, forceWebView: false);
    } else {
      throw 'Could not launch $url';
    }
  }

  Future<void> _launchInWebViewOrVC(String url) async {
    if (await canLaunch(url)) {
      await launch(url, forceSafariVC: true, forceWebView: true);
    } else {
      throw 'Could not launch $url';
    }
  }

  Widget _launchStatus(BuildContext context, AsyncSnapshot<void> snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else {
      return Text('');
    }
  }

  @override
  Widget build(BuildContext context) {
    String toLaunch = 'https://flutter.dev';
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(16.0),
              child: Text(toLaunch),
            ),
            ElevatedButton(
              onPressed: () => setState(() {
                    _launched = _launchInBrowser(toLaunch);
                  }),
              child: Text('Launch in browser'),
            ),
            Padding(padding: EdgeInsets.all(16.0)),
            ElevatedButton(
              onPressed: () => setState(() {
                    _launched = _launchInWebViewOrVC(toLaunch);
                  }),
              child: Text('Launch in app'),
            ),
            Padding(padding: EdgeInsets.all(16.0)),
            FutureBuilder<void>(future: _launched, builder: _launchStatus),
          ],
        ),
      ),
    );
  }
}
name: flutter_app
description: A new Flutter application.
version: 1.0.0+1

dependencies:
  flutter:
    sdk: flutter

  url_launcher: ^3.0.3
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

Dart 和原生调试器都在与同一个进程交互。使用其中一个或者同时使用两个来设置断点、检查堆栈、恢复运行…… 换句话说,调试!

Both the Dart and OEM debuggers are interacting with the same process. User either, or both, to set breakpoints, examine stack, resume execution… In other words, debug!

screenshot of Android Studio in the Dart debug pane.

Dart 调试面板和 `lib/main.dart` 中的两个断点。
Dart 调试面板和 `lib/main.dart` 中的两个断点。

screenshot of Android Studio in the Android debug pane.

Android 调试面板和 `GeneratedPluginRegistrant.java` 中的一个断点。通过单击调试面板顶部的相应调试器,在调试器之间进行切换。

使用 Xcode 调试(iOS)

Debugging with Xcode (iOS)

为了调试原生 iOS 代码,你需要一个包含原生 iOS 代码的应用。在本节中,你将学会如何连接两个调试器到你的应用: 1)Dart 调试器 2)Xcode 调试器。

In order to debug OEM iOS code, you need an app that contains OEM iOS code. In this section, you’ll learn how to connect two debuggers to your app: 1) the Dart debugger and, 2) the Xcode debugger.

[PENDING]

资源

Resources

下面的资源包含更多关于 Flutter、iOS 和 Android 调试的信息。

The following resources have more information on debugging Flutter, iOS, and Android:

Flutter

Android

你可以在 developer.android.com 找到下列的调试资源。

You can find the following debugging resources on developer.android.com.

iOS

你可以在 developer.apple.com 找到下列的调试资源。

You can find the following debugging resources on developer.apple.com.

Flutter 的构建模式选择

目录

Flutter 支持三种模式编译 app,也支持使用 headless 模式来测试。这篇文档解释了这三种模式,并且告诉你什么时候应该使用哪种模式。关于 headless 测试的更多信息,可以查看 单元测试

The Flutter tooling supports three modes when compiling your app, and a headless mode for testing. This doc explains the three modes and tells you when to use which. For more information on headless testing, see Unit testing.

选择哪种编译模式取决于你处于哪个开发周期中。是调试代码阶段,还是需要性能优化分析,抑或是准备部署你的应用了呢?

You choose a compilation mode depending on where you are in the development cycle. Are you debugging your code? Do you need profiling information? Are you ready to deploy your app?

快速简要介绍下列三种构建模式:

A quick summary for when to use which mode is as follows:

下文详细解释了每种模式以及何时使用它,获得更多信息,或者了解无头模式的测试,请参考 Flutter wiki 文档。

The rest of the page goes into more detail about these modes. For information on headless testing, see the Flutter wiki.

调试模式

Debug

Debug 模式下,app 可以被安装在物理设备、仿真器或者模拟器上进行调试。

In debug mode, the app is set up for debugging on the physical device, emulator, or simulator.

Debug 模式意味着:

Debug mode for mobile apps mean that:

在 Web 平台下的调试模式意味着:

Debug mode for a web app means that:

默认情况下,运行 flutter run 会使用 Debug 模式。你的 IDE 也支持这些模式。例如,Android Studio 提供了 Run > Debug… 菜单选项,而且在项目面板中还有一个三角形的绿色运行按钮图标(菜单选项中会显示相应图标的图片)。

By default, flutter run compiles to debug mode. Your IDE supports this mode. Android Studio, for example, provides a Run > Debug… menu option, as well as a green bug icon overlayed with a small triangle on the project page.

Release 模式

Release

当你想要最大的优化以及最小的占用空间时,就使用 Release 模式来部署 app 吧。 release 模式是不支持模拟器或者仿真器的,使用 Release 模式意味着:

Use release mode for deploying the app, when you want maximum optimization and minimal footprint size. For mobile, release mode (which is not supported on the simulator or emulator), means that:

在 Web 平台的 Release 模式意味着:

Release mode for a web app means that:

flutter run --release 命令会使用 Release 模式来进行编译。你的 IDE 同样也支持这个模式。例如,Android Studio 提供了 Run > Run… 菜单选项,而且在项目面板中还有一个被小三角覆盖的绿色虫子图标。(菜单选项中会显示相应图标的图片)

The command flutter run --release compiles to release mode. Your IDE supports this mode. Android Studio, for example, provides a Run > Run… menu option, as well as a triangular green run button icon on the project page. (The menu item shows a pic of the corresponding icon.)

你可以使用 flutter build <target> 针对特定目标编译 release 模式。请使用 flutter help build 查看支持的目标列表。

You can compile to release mode for a specific target with flutter build <target>. For a list of supported targets, use flutter help build.

你也可以通过 flutter build --release 命令来使用 release 模式。

You can also compile to release mode with flutter build --release.

你也可以运行 flutter build 命令使用 Release 模式来编译。更多详细信息,可以参阅发布 iOSAndroid app 的文档。

For more information, see the docs on releasing iOS and Android apps.

Profile 模式

Profile

profile 模式下,一些调试能力是被保留的—足够分析你的 app 性能。在仿真器和模拟器上,Profile 模式是不可用的,因为他们的行为不能代表真实的性能。 profile 模式和 release 类似,但有以下不同:

In profile mode, some debugging ability is maintained—enough to profile your app’s performance. Profile mode is disabled on the emulator and simulator, because their behavior is not representative of real performance. On mobile, profile mode is similar to release mode, with the following differences:

在 Web 平台的 Profile 模式意味着:

Profile mode for a web app means that:

flutter run --profile 命令是使用 Profile 模式来编译的。你的 IDE 也是支持这个模式的。例如,Android Studio 提供了 Run > Profile… 菜单选项。

Your IDE supports this mode. Android Studio, for example, provides a Run > Profile… menu option. The command flutter run --profile compiles to profile mode.

关于这些模式的更多信息,可以查看 Flutter wiki 中的 Flutter’s build modes 文档。

For more information on the build modes, see Flutter’s build modes.

Flutter 开发中常见的报错

目录

Introduction

This page explains several frequently-encountered Flutter framework errors and gives suggestions on how to resolve them. This is a living document with more errors to be added in future revisions, and your contributions are welcomed. Feel free to open an issue or submit a pull request to make this page more useful to you and the Flutter community.

‘A RenderFlex overflowed…’

RenderFlex overflow is one of the most frequently encountered Flutter framework errors, and you probably have run into it already.

What does the error look like?

When it happens, you’ll see yellow & black stripes indicating the area of overflow in the app UI in addition to the error message in the debug console:

The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.

The relevant error-causing widget was
    Row 	    lib/errors/renderflex_overflow_column.dart:23
The overflowing RenderFlex has an orientation of Axis.horizontal.
The edge of the RenderFlex that is overflowing has been marked in the rendering 
with a yellow and black striped pattern. This is usually caused by the contents 
being too big for the RenderFlex.
(Additional lines of this message omitted)

How might you run into this error?

The error often occurs when a Column or Row has a child widget that is not constrained in its size. For example, the code snippet below demonstrates a common scenario:

Widget build(BuildContext context) {
  return Container(
    child: Row(
      children: [
        Icon(Icons.message),
        Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text("Title", style: Theme.of(context).textTheme.headline4),
            Text(
                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed"
                " do eiusmod tempor incididunt ut labore et dolore magna "
                "aliqua. Ut enim ad minim veniam, quis nostrud "
                "exercitation ullamco laboris nisi ut aliquip ex ea "
                "commodo consequat."),
          ],
        ),
      ],
    ),
  );
}

In the above example, the Column tries to be wider than the space the Row (its parent) can allocate to it, causing an overflow error. Why does the Column try to do that? To understand this layout behavior, you need to know how Flutter framework performs layout:

To perform layout, Flutter walks the render tree in a depth-first traversal and passes down size constraints from parent to child… Children respond by passing up a size to their parent object within the constraints the parent established.” – Flutter architectural overview

In this case, the Row widget doesn’t constrain the size of its children, nor does the Column widget. Lacking constraints from its parent widget, the second Text widget tries to be as wide as all the characters it needs to display. The self-determined width of the Text widget then gets adopted by the Column which clashes with the maximum amount of horizontal space its parent the Row widget can provide.

How to fix it?

Well, you need to make sure the Column won’t attempt to be wider than it can be. To achieve this, you need to constrain its width. One way to do it is to wrap the Column in an Expanded widget:

child: Row(
  children: [
    Icon(Icons.message),
    Expanded(
      child: Column(
        // code omitted
      ),
    ),
  ]
)

Another way is to wrap the Column in a Flexible widget and specify a flex factor. In fact, the Expanded widget is equivalent to the Flexible widget with a flex factor of 1.0, as its source code shows. To further understand how to use the Flex widget in Flutter layouts, please check out this Widget of the Week video on the Flexible widget.

Further information:

The resources linked below provide further information about this error.

‘RenderBox was not laid out’

While this error is pretty common, it’s often a side effect of a primary error occurring earlier in the rendering pipeline.

What does the error look like?

The message shown by the error looks like this:

RenderBox was not laid out: 
RenderViewport#5a477 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE

How might you run into this error?

Usually, the issue is related to violation of box constraints, and it needs to be solved by providing more information to Flutter about how you’d like to constrain the widgets in question. You can learn more about how constraints work in Flutter on the page Understanding constraints.

The RenderBox was not laid out error is often caused by one of two other errors:

‘Vertical viewport was given unbounded height’

This is another common layout error you could run into while creating a UI in your Flutter app.

What does the error look like?

The message shown by the error looks like this:

The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.

Viewports expand in the scrolling direction to fill their container. 
In this case, a vertical viewport was given an unlimited amount of 
vertical space in which to expand. This situation typically happens when a 
scrollable widget is nested inside another scrollable widget.
(Additional lines of this message omitted)

How might you run into this error?

The error is often caused when a ListView (or other kinds of scrollable widgets such as GridView) is placed inside a Column. A ListView takes all the vertical space available to it, unless it’s constrained by its parent widget. However, a Column doesn’t impose any constraint on its children’s height by default. The combination of the two behaviors leads to the failure of determining the size of the ListView.

Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        Text('Header'),
        ListView(
          children: <Widget>[
            ListTile(
              leading: Icon(Icons.map),
              title: Text('Map'),
            ),
            ListTile(
              leading: Icon(Icons.subway),
              title: Text('Subway'),
            ),
          ],
        ),
      ],
    ),
  );
}

How to fix it?

To fix this error, specify how tall the ListView should be. To make it as tall as the remaining space in the Column, wrap it using an Expanded widget (see the example below). Otherwise, specify an absolute height using a SizedBox widget or a relative height using a Flexible widget.

Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        Text('Header'),
        Expanded(
          child: ListView(
            children: <Widget>[
              ListTile(
                leading: Icon(Icons.map),
                title: Text('Map'),
              ),
              ListTile(
                leading: Icon(Icons.subway),
                title: Text('Subway'),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

Further information:

The resources linked below provide further information about this error.

‘An InputDecorator…cannot have an unbounded width’

The error message suggests that it’s also related to box constraints, which are important to understand to avoid many of the most common Flutter framework errors.

What does the error look like?

The message shown by the error looks like this:

The following assertion was thrown during performLayout():
An InputDecorator, which is typically created by a TextField, cannot have an 
unbounded width.
This happens when the parent widget does not provide a finite width constraint. 
For example, if the InputDecorator is contained by a `Row`, then its width must 
be constrained. An `Expanded` widget or a SizedBox can be used to constrain the 
width of the InputDecorator or the TextField that contains it.
(Additional lines of this message omitted)

How might you run into the error?

This error occurs, for example, when a Row contains a TextFormField or a TextField but the latter has no width constraint.

Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title:
            Text('Unbounded Width of the TextField'),
      ),
      body: Row(
        children: [
          TextField(),
        ],
      ),
    ),
  );
}

How to fix it?

As suggested by the error message, fix this error by constraining the text field using either an Expanded or SizedBox widget. The following example demonstrates using an Expanded widget:

  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Unbounded Width of the TextField'),
        ),
        body: Row(
          children: [
            Expanded(
                child: TextFormField()
            ),
          ],
        ),
      ),
    );
  }

‘Incorrect use of ParentData widget’

This error is about missing an expected parent widget.

What does the error look like?

The message shown by the error looks like this:

The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
(Some lines of this message omitted)
Usually, this indicates that at least one of the offending ParentDataWidgets 
listed above is not placed directly inside a compatible ancestor widget.

How might you run into the error?

While Flutter’s widgets are generally flexible in how they can be composed together in a UI, a small subset of those widgets expect specific parent widgets. When this expectation can’t be satisfied in your widget tree, you’re likely to see this error.

Here is an incomplete list of widgets that expect specific parent widgets within the Flutter framework. Feel free to submit a PR (using the doc icon in the top right corner of the page) to expand this list.

Widget Expected parent widget(s)
Flexible Row, Column, or Flex
Expanded (a specialized Flexible) Row, Column, or Flex
Positioned Stack
TableCell Table

How to fix it?

The fix should be obvious once you know which parent widget is missing.

‘setState called during build’

The build method in your Flutter code is not a good place to call setState either directly or indirectly.

What does the error look like?

When the error occurs, the following message gets displayed in the console:

The following assertion was thrown building DialogPage(dirty, dependencies: 
[_InheritedTheme, _LocalizationsScope-[GlobalKey#59a8e]], 
state: _DialogPageState#f121e):
setState() or markNeedsBuild() called during build.

This Overlay widget cannot be marked as needing to build because the framework 
is already in the process of building widgets.
(Additional lines of this message omitted)

How might you run into the error?

In general, this error occurs when the setState method is called within the build method.

A common scenario where this error occurs is when attempting to trigger a Dialog from within the build method. This is often motivated by the need to immediately show information to the user, but setState should never be called from a build method.

Below is a snippet that seems to be a common culprit of this error:

Widget build(BuildContext context) {

  // Don't do this. 
  showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text("Alert Dialog"),
        );
      });

  return Center(
    child: Column(
      children: <Widget>[
        Text('Show Material Dialog'),
      ],
    ),
  );
}

You don’t see the explicit call to setState, but it’s called by showDialog. The build method is not the right place to call showDialog because build can be called by the framework for every frame, for example, during an animation.

How to fix it?

One way to avoid this error is to use the Navigator API to trigger the dialog as a route. In the example below, there are two pages. The second page has a dialog to be displayed upon entry. When the user requests the second page from clicking on a button on the first page, the Navigator pushes two routes in a row – one for the second page and another for the dialog.

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen using a named route.
            Navigator.pushNamed(context, '/second');
            // Immediately show a dialog upon loading the second screen.
            Navigator.push(
              context,
              PageRouteBuilder(
                barrierDismissible: true,
                opaque: false,
                pageBuilder: (_, anim1, anim2) => MyDialog(),
              ),
            );
          },
        ),
      ),
    );
  }
}

References

To learn more about how to debug errors, especially layout errors in Flutter, check out the following resources:

在 Flutter 里处理错误

目录

Flutter 框架可以捕获运行期间的错误,包括构建期间、布局期间和绘制期间。

The Flutter framework catches errors that occur during callbacks triggered by the framework itself, including errors encountered during the build, layout, and paint phases. Errors that don’t occur within Flutter’s callbacks can’t be caught by the framework, but you can handle them by setting up a Zone.

所有 Flutter 的错误均会被回调方法 FlutterError.onError 捕获。默认情况下,会调用 FlutterError.presentError 方法,并将错误转储到当前的设备日志中。当从 IDE 运行应用时,检查器重写了该方法,错误也被发送到 IDE 的控制台,可以在控制台中检查出错的对象。

All errors caught by Flutter are routed to the FlutterError.onError handler. By default, this calls FlutterError.presentError, which dumps the error to the device logs. When running from an IDE, the inspector overrides this behavior so that errors can also be routed to the IDE’s console, allowing you to inspect the objects mentioned in the message.

当构建期间发生错误时,回调函数 ErrorWidget.builder 会被调用,来生成一个新的 widget,用来代替构建失败的 widget。默认情况,debug 模式下会显示一个红色背景的错误页面, release 模式下会展示一个灰色背景的空白页面。

When an error occurs during the build phase, the ErrorWidget.builder callback is invoked to build the widget that is used instead of the one that failed. By default, in debug mode this shows an error message in red, and in release mode this shows a gray background.

如果在调用堆栈上没有 Flutter 回调的情况下发生错误,它们由发生区域的 Zone 处理。 Zone 在默认情况下仅会打印错误,而不会执行其他任何操作。

When errors occur without a Flutter callback on the call stack, they are handled by the Zone where they occur. By default, a Zone only prints errors and does nothing else.

这些回调方法都可以被重写,通常在 void main() 方法中重写。

You can customize these behaviors, typically by setting them to values in your void main() function.

下面解释了所有的错误捕获类型。在最后的代码段可以用于处理所有类型的错误。尽管你可以直接复制粘贴代码段,但我们建议你先了解每种错误类型。

Below each error type handling is explained. At the bottom there’s a code snippet which handles all types of errors. Even though you can just copy-paste the snippet, we recommend you to first get acquainted with each of the error types.

Flutter 导致的错误

Errors caught by Flutter

例如,如果你想在 release 模式下发生错误时立刻关闭应用,可以使用下面的回调方法:

For example, to make your application quit immediately any time an error is caught by Flutter in release mode, you could use the following handler:

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    if (kReleaseMode)
      exit(1);
  };
  runApp(MyApp());
}

// rest of `flutter create` code...

这个回调方法也可以上报错误到日志服务平台。更多信息可以查看文档 报错信息通过服务上传

This handler can also be used to report errors to a logging service. For more details, see our cookbook chapter for reporting errors to a service.

自定义一个 ErrorWidget 以展示 build 时的错误

Define a custom error widget for build phase errors

定义一个自定义的 error widget,以当 builder 构建 widget 失败时显示,请使用 MaterialApp.builder

To define a customized error widget that displays whenever the builder fails to build a widget, use MaterialApp.builder.

class MyApp extends StatelessWidget {
...
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      builder: (BuildContext context, Widget widget) {
        Widget error = Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator)
          error = Scaffold(body: Center(child: error));
        ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
        return widget;
      },
    );
  }
}

未被 Flutter 捕获的错误

Errors not caught by Flutter

假设一个 onPressed 回调调用了异步方法,例如 MethodChannel.invokeMethod (或者其他 plugin 的方法):

Consider an onPressed callback that invokes an asynchronous function, such as MethodChannel.invokeMethod (or pretty much any plugin). For example:

OutlinedButton(
  child: Text('Click me!'),
  onPressed: () async {
    final channel = const MethodChannel('crashy-custom-channel');
    await channel.invokeMethod('blah');
  },
),

如果 invokeMethod 抛出了错误,它不会传递至 FlutterError.onError,而是直接进入 runAppZone

If invokeMethod throws an error, it won’t be forwarded to FlutterError.onError. Instead, it’s forwarded to the Zone where runApp was run.

如果你想捕获这样的错误,请使用 runZonedGuarded

To catch such an error, use runZonedGuarded.

import 'dart:async';

void main() {
  runZonedGuarded(() {
    runApp(MyApp());
  }, (Object error, StackTrace stack) {
    myBackend.sendError(error, stack);
  });
}

请注意,如果你的应用在 runApp 中调用了 WidgetsFlutterBinding.ensureInitialized() 方法来进行一些初始化操作(例如 Firebase.initializeApp()),则必须在 runZonedGuarded 中调用 WidgetsFlutterBinding.ensureInitialized()

Note that if in your app you call WidgetsFlutterBinding.ensureInitialized() manually to perform some initialization before calling runApp (e.g. Firebase.initializeApp()), you must call WidgetsFlutterBinding.ensureInitialized() inside runZonedGuarded:

runZonedGuarded(() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

处理所有类型的错误

Handling all types of errors

如果你想在异常抛出时退出应用,并在 build 错误时展示自定义的 ErrorWidget,你可以在下面的代码片段的基础上定制你的处理:

Say you want to exit application on any exception and to display a custom error widget whenever a widget building fails - you can base your errors handling on next code snippet:

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await myErrorsHandler.initialize();
    FlutterError.onError = (FlutterErrorDetails details) {
      FlutterError.presentError(details);
      myErrorsHandler.onError(details);
      exit(1);
    };
    runApp(MyApp());
  }, (Object error, StackTrace stack) {
    myErrorsHandler.onError(error, stack);
    exit(1);
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (BuildContext context, Widget widget) {
        Widget error = Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator)
          error = Scaffold(body: Center(child: error));
        ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
        return widget;
      },
    );
  }
}

测试 Flutter 应用

目录

通常一个应用的功能越多,手工测试就越困难。自动化测试在发布之前运行,有助于保证我们应用的稳定性和功能的完整性,并且可以快速修复问题。

The more features your app has, the harder it is to test manually. Automated tests help ensure that your app performs correctly before you publish it, while retaining your feature and bug fix velocity.

自动化测试可分为以下几类:

Automated testing falls into a few categories:

一般来说,在自动化测试方面做的比较好的应用会有许多单元测试和 widget 测试,并且使用 代码覆盖率 进行追踪,还会有足够的集成测试来覆盖所有的重要使用场景。这样做是因为不同类型的测试之间需要权衡,如下所示:

Generally speaking, a well-tested app has many unit and widget tests, tracked by code coverage, plus enough integration tests to cover all the important use cases. This advice is based on the fact that there are trade-offs between different kinds of testing, seen below.

  单元测试Unit Widget 测试Widget 集成测试Integration

置信度

Confidence

Low

较高

Higher

最高

Highest

维护成本

Maintenance cost

Low

较高

Higher

最高

Highest

依赖程度

Dependencies

Few

较多

More

最多

Most

执行速度

Execution speed

Quick

较慢

Slower

Slow

单元测试

Unit tests

单元测试 测试单一的函数,方法或类。单元测试的目标是验证逻辑单元在各种条件下的正确性。被测试单元的外部依赖通常需要 模拟。单元测试通常不会读写磁盘,将数据渲染到屏幕,也不会从运行测试进程的外部去接收用户的操作。你可以在终端执行 flutter test --help 命令获得更多有关单元测试的帮助:

A unit test tests a single function, method, or class. The goal of a unit test is to verify the correctness of a unit of logic under a variety of conditions. External dependencies of the unit under test are generally mocked out. Unit tests generally don’t read from or write to disk, render to screen, or receive user actions from outside the process running the test. For more information regarding unit tests, you can view the following recipes or run flutter test --help in your terminal.

更多信息

Recipes

Widget测试

Widget tests

Widget 测试(在其他 UI 框架中指 组件测试)是用来测试单一的 widget, widget 测试的目标是验证 widget 的 UI 表现和交互行为是否符合预期。测试一个 widget 涉及多个类,并且测试环境需要提供具有 widget 生命周期的上下文。

例如,被测试的 widget 可以接收和响应用户操作和事件,进行布局并实例化子 widget。所以,widget 测试比单元测试更全面。但是,就像单元测试一样,widget 测试环境实现上比成熟的 UI 系统简单得多。

A widget test (in other UI frameworks referred to as component test) tests a single widget. The goal of a widget test is to verify that the widget’s UI looks and interacts as expected. Testing a widget involves multiple classes and requires a test environment that provides the appropriate widget lifecycle context.

For example, the Widget being tested should be able to receive and respond to user actions and events, perform layout, and instantiate child widgets. A widget test is therefore more comprehensive than a unit test. However, like a unit test, a widget test’s environment is replaced with an implementation much simpler than a full-blown UI system.

更多信息

Recipes

集成测试

Integration tests

集成测试 测试一个完整的应用或者一个应用的大部分功能。集成测试的目标是验证正在测试的所有 widget 和服务是否按照预期的方式一起工作。此外,还可以使用集成测试来验证应用的性能。

通常情况下,一个 集成测试 运行在真机或 OS 模拟器上,如 iOS 模拟器 (iOS Simulator) 或 Android 模拟器 (Android Emulator) 。测试中的应用通常与测试驱动程序代码隔离,以避免结果出现偏差。

An integration test tests a complete app or a large part of an app. The goal of an integration test is to verify that all the widgets and services being tested work together as expected. Furthermore, you can use integration tests to verify your app’s performance.

更多关于如何编写集成测试的相关信息,请参阅集成测试文档

Generally, an integration test runs on a real device or an OS emulator, such as iOS Simulator or Android Emulator. The app under test is typically isolated from the test driver code to avoid skewing the results.

For more information on how to write integration tests, see the integration testing page.

更多信息

Recipes

持续集成服务

Continuous integration services

持续集成 (CI) 服务允许我们在推送新代码(代码变更)时自动运行测试。当代码变更后,会立即收到关于代码是否仍按预期工作、是否引入新问题的反馈。

有关各种持续集成服务的信息,参考如下:

Continuous integration (CI) services allow you to run your tests automatically when pushing new code changes. This provides timely feedback on whether the code changes work as expected and do not introduce bugs.

For information on running tests on various continuous integration services, see the following:

集成测试

目录

This page describes how to use the integration_test package to run integration tests. Tests written using this package have the following properties:

Overview

Unit tests, widget tests, and integration tests

There are three types of tests that Flutter supports. A unit test verifies the behavior of a method or class. A widget test verifies the behavior of Flutter widgets without running the app itself. An integration test (also called end-to-end testing or GUI testing) runs the full app.

Hosts and targets

During development, you are probably writing the code on a desktop computer, called the host machine, and running the app on a mobile device, browser, or desktop application, called the target device. (If you are using a web browser or desktop application, the host machine is also the target device.)

integration_test

Tests written with the integration_test package can:

  1. Run directly on the target device, allowing you to test on multiple Android or iOS devices using Firebase Test Lab.
  2. Run using flutter test integration_test.
  3. Use flutter_test APIs, making integration tests more like writing widget tests.

Migrating from flutter_driver

Existing projects using flutter_driver can be migrated to integration_test by following the Migrating from flutter_drive guide.

Project setup

Add integration_test and flutter_test to your pubspec.yaml file:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter

In your project, create a new directory integration_test/ with a new file, <name>_test.dart:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  testWidgets("failing test example", (WidgetTester tester) async {
    expect(2 + 2, equals(5));
  });
}

Directory structure

lib/
  ...
integration_test/
  foo_test.dart
  bar_test.dart
test/
  # Other unit tests go here.

See also:

Running using the flutter command

These tests can be launched with the flutter test command, where <DEVICE_ID>: is the optional device ID or pattern displayed in the output of the flutter devices command:

flutter test integration_test/foo_test.dart -d <DEVICE_ID>

This runs the tests in foo_test.dart. To run all tests in this directory on the default device, run:

flutter test integration_test

Running in a browser

First, Download and install ChromeDriver and run it on port 4444:

chromedriver --port=4444

To run tests with flutter drive, create a new directory containing a new file, test_driver/integration_test.dart:

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

Then add IntegrationTestWidgetsFlutterBinding.ensureInitialized() in your integration_test/<name>_test.dart file:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // NEW

  testWidgets("failing test example", (WidgetTester tester) async {
    expect(2 + 2, equals(5));
  });
}

In a separate process, run flutter_drive:

flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/counter_test.dart \
  -d web-server

To learn more, see the Running Flutter driver tests with web wiki page.

Testing on Firebase Test Lab

You can use the Firebase Test Lab with both Android and iOS targets.

Android setup

Follow the instructions in the Android Device Testing section of the README.

iOS setup

Follow the instructions in the iOS Device Testing section of the README.

Test Lab project setup

Go to the Firebase Console, and create a new project if you don’t have one already. Then navigate to Quality > Test Lab:

Firebase Test Lab Console

Uploading an Android APK

Create an APK using Gradle:

pushd android
# flutter build generates files in android/ for building the app
flutter build apk
./gradlew app:assembleAndroidTest
./gradlew app:assembleDebug -Ptarget=integration_test/<name>_test.dart
popd

Where <name>_test.dart is the file created in the Project Setup section.

Drag the “debug” APK from <flutter_project_directory>/build/app/outputs/apk/debug into the Android Robo Test target on the web page. This starts a Robo test and allows you to run other tests:

Firebase Test Lab upload

Click Run a test, select the Instrumentation test type and drag the following two files:

Firebase Test Lab upload two APKs

If a failure occurs, you can view the output by selecting the red icon:

Firebase Test Lab test results

Uploading an Android APK from the command line

See the Firebase Test Lab section of the README for instructions on uploading the APKs from the command line.

Uploading Xcode tests

See the Firebase TestLab iOS instructions for details on how to upload the .zip file to the Firebase TestLab section of the Firebase Console.

Uploading Xcode tests from the command line

See the iOS Device Testing section in the README for instructions on how to upload the .zip file from the command line.

性能评估

目录

Flutter 性能入门

什么是性能?为什么性能很重要?如何才能提升性能?

Flutter performance basics

我们的目标是回答这三个问题(主要是第三个)以及任何与之相关的话题。如果你有任何关于性能方面的问题,本文档可以作为解决你疑惑的起点。

What is performance? Why is performance important? How do I improve performance?

前两个问题的答案比较哲学,对于正在阅读这篇文章的开发者而言,当他们需要解决特定的性能问题时,并没有什么帮助。所以,我们将它们放在了 附录

Our goal is to answer those three questions (mainly the third one), and anything related to them. This document should serve as the single entry point or the root node of a tree of resources that addresses any questions that you have about performance.

为了提升性能,首先你需要一些可以量化的指标来验证问题和性能的提升。在 指标 页面,你可以看到一些现有的指标,以及哪些工具和 API 可以用于获取这些指标。

The answers to the first two questions are mostly philosophical, and not as helpful to many developers who visit this page with specific performance issues that need to be solved. Therefore, the answers to those questions are in the appendix.

这里有一个 常见问题 的列表,你可以查询你的问题是否出现过或者已经被解答,以及是否有现成的解决方案。(你也可以查看 GitHub issues 里含有 性能 标签的内容。)

To improve performance, you first need metrics: some measurable numbers to verify the problems and improvements. In the metrics page, you’ll see which metrics are currently used, and which tools and APIs are available to get the metrics.

最后,性能问题可以分为四类,对应 GitHub issue 里的四个标签:「流畅度」、「内存」、「应用大小」、和「功耗」。

There is a list of Frequently asked questions, so you can find out if the questions you have or the problems you’re having were already answered or encountered, and whether there are existing solutions. (Alternatively, you can check the Flutter GitHub issue database using the performance label.)

其它内容均已归纳到这四个类别中。(注意:这些文档正在扩展中。)

Finally, the performance issues are divided into four categories. They correspond to the four labels that are used in the Flutter GitHub issue database: “perf: speed”, “perf: memory”, “perf: app size”, “perf: energy”.

The rest of the content is organized using those four categories. (Note that these docs are in the process of being expanded.)

流畅度

Speed

你的动画是否卡顿(不流畅)?学习如何评估和修复渲染问题。

提高渲染性能

Are your animations janky (not smooth)? Learn how to evaluate and fix rendering issues.

Improving rendering performance

内存

Memory

明智地使用内存

Using memory wisely

应用大小

App size

如何测量应用的体积。体积越小,下载就越快。

测量应用的体积

How to measure your app’s size. The smaller the size, the quicker it is to download.

Measuring your app’s size

测量你的应用体积

目录

许多开发者都会关注应用编译后的大小。Flutter 应用编译出的 APK、app bundle 和 IPA 均持有应用运行需要的所有代码和资源,是完全独立的。一个应用越大,在设备上占用的空间就越多,下载时间就越长,还可能超出 Android 即时应用等实用功能的限制。

Many developers are concerned with the size of their compiled app. As the APK, app bundle, or IPA version of a Flutter app is self-contained and holds all the code and assets needed to run the app, its size can be a concern. The larger an app, the more space it requires on a device, the longer it takes to download, and it may break the limit of useful features like Android instant apps.

调试版本不具有代表性

Debug builds are not representative

默认情况下,使用 flutter run 命令启动应用,或者点击 IDE 的 Play 按钮(如 开发体验初探编写第一个 Flutter 应用 中所使用的),会生成 Flutter 应用的 调试 版本。调试版本体积很大,用于热重载和源码调试。因此,它不能代表用户最终下载的正式版本的应用。

By default, launching your app with flutter run, or by clicking the Play button in your IDE (as used in Test drive and Write your first Flutter app), generates a debug build of the Flutter app. The app size of a debug build is large due to the debugging overhead that allows for hot reload and source-level debugging. As such, it is not representative of a production app end users download.

检查总大小

Checking the total size

flutter build apkflutter build ios 等生成的默认发行版本,是为了方便在 Play 商店和 App Store 上组装你上传的应用包。因此,它们也无法代表你的用户最终下载的大小。应用商店通常会针对不同的下载程序及其硬件,重新处理和拆分你上传的应用包,例如根据手机的 DPI 过滤资源、根据手机的 CPU 架构过滤原生库。

A default release build, such as one created by flutter build apk or flutter build ios, is built to conveniently assemble your upload package to the Play Store and App Store. As such, they’re also not representative of your end-users’ download size. The stores generally reprocess and split your upload package to target the specific downloader and the downloader’s hardware, such as filtering for assets targeting the phone’s DPI, filtering native libraries targeting the phone’s CPU architecture.

估算总大小

Estimating total size

请使用以下指南,获取各个平台下最接近的估算大小。

To get the closest approximate size on each platform, use the following instructions.

Android

根据 Google 的 Play 控制台说明 来查看应用的下载大小。

Follow the Google Play Console’s instructions for checking app download and install sizes.

生成你的应用的上传包:

Produce an upload package for your application:

flutter build appbundle

登录你的 Google Play 控制台。通过拖放 .abb 文件来上传应用的二进制文件。

Log into your Google Play Console. Upload your application binary by drag dropping the .aab file.

Android vitals -> App size 选项卡中查看应用的下载和安装大小。

View the application’s download and install size in the Android vitals -> App size tab.

App size tab in Google Play Console

该下载大小是基于 XXXHDPI (~640dpi) 且架构为 arm64-v8a 的设备来计算的。用户最终的下载大小可能因硬件而异。

The download size is calculated based on an XXXHDPI (~640dpi) device on an arm64-v8a architecture. Your end users’ download sizes may vary depending on their hardware.

顶部选项卡有一个切换下载大小和安装大小的开关。该页面还包含了进一步的优化提示。

The top tab has a toggle for download size and install size. The page also contains optimization tips further below.

iOS

创建一份 Xcode 应用大小报告

Create an Xcode App Size Report.

首先,参照 iOS 创建构建归档指南,配置应用的版本,并开始构建。

First, by configuring the app version and build as described in the iOS create build archive instructions.

然后:

Then:

  1. 选择 Product > Archive 生成一个构建归档。

    Select Product > Archive to produce a build archive.

  2. 在 Xcode Organizer 窗口的侧栏,选择你的 iOS 应用,然后选择你刚刚生成的构建归档。

    In the sidebar of the Xcode Organizer window, select your iOS app, then select the build archive you just produced.

  3. 点击 Distribute App

    Click Distribute App.

  4. 选择一种发布方式。如果你不打算发布该应用,Development 模式是最简单的。

    Select a method of distribution. Development is the simplest if you don’t intend to distribute the application.

  5. App Thinning 中,选择「all compatible device variants」。

    In App Thinning, select ‘all compatible device variants’.

  6. 选择 Rebuild from Bitcode(如果你的项目启用了 bitcode)。

    Select Rebuild from Bitcode (available if bitcode is enabled on your project).

  7. 选择 Strip Swift symbols

    Select Strip Swift symbols.

签名并导出 IPA 包。导出目录中有一个「App Thinning Size Report.txt」文件,其中记录了在不同设备和 iOS 版本上预估的应用程序大小的详细信息。

Sign and export the IPA. The exported directory contains App Thinning Size Report.txt with details about your projected application size on different devices and versions of iOS.

Flutter 1.17 上的默认 demo app 的应用大小报告显示如下:

The App Size Report for the default demo app in Flutter 1.17 shows:

Variant: Runner-7433FC8E-1DF4-4299-A7E8-E00768671BEB.ipa
Supported variant descriptors: [device: iPhone12,1, os-version: 13.0] and [device: iPhone11,8, os-version: 13.0]
App + On Demand Resources size: 5.4 MB compressed, 13.7 MB uncompressed
App size: 5.4 MB compressed, 13.7 MB uncompressed
On Demand Resources size: Zero KB compressed, Zero KB uncompressed

在这个例子中,设备 iPhone12,1(iPhone 11 的 Model ID / Hardware number)和 iPhone11,8 (iPhone XR) 运行在 iOS 13.0 版本下时,下载大小约为 5.4 MB,安装大小约为 13.7 MB。

In this example, the app has an approximate download size of 5.4 MB and an approximate install size of 13.7 MB on an iPhone12,1 (Model ID / Hardware number for iPhone 11) and iPhone11,8 (iPhone XR) running iOS 13.0.

想要精确测量一个 iOS 应用的体积,你需要先将一个发行版本的 IPA 包上传至 App Store Connect(简介),再获取它的大小报告。 IPA 包一般都比 APK 包要大,这在 Flutter FAQ 中的 Flutter 引擎有多大? 一节中已经阐述过了。

To measure an iOS app exactly, you have to upload a release IPA to Apple’s App Store Connect (instructions) and obtain the size report from there. IPAs are commonly larger than APKs as explained in How big is the Flutter engine?, a section in the Flutter FAQ.

大小拆分

Breaking down the size

从 Flutter 1.22 和 DevTools 0.9.1 版本开始,包含了一个大小分析工具,帮助开发者了解和拆分应用的发行版本。

Starting in Flutter version 1.22 and DevTools version 0.9.1, a size analysis tool is included to help developers understand the breakdown of the release build of their application.

该大小分析工具通过在构建时添加 --analyze-size 标记来调用:

The size analysis tool is invoked by passing the --analyze-size flag when building:

这种构建模式和标准的发行构建相比,有以下两方面的区别:

This build is different from a standard release build in two ways.

  1. 该工具编译 Dart 时,记录了 Dart 包的代码大小使用情况。

    The tool compiles Dart in a way that records code size usage of Dart packages.

  2. 该工具在终端上展示了大小拆分的摘要信息,并在 DevTools 中生成了一个 *-code-size-analysis_*.json 文件,用于进行更详细的分析。

    The tool displays a high level summary of the size breakdown in the terminal, and leaves a *-code-size-analysis_*.json file for more detailed analysis in DevTools.

除了分析单个构建,你还可以在 DevTools 中加载两个 *-code-size-analysis_*.json 文件比较差异。详情请阅读 DevTools 文档

In addition to analyzing a single build, two builds can also be diffed by loading two *-code-size-analysis_*.json files into DevTools. See DevTools documentation for details.

Size summary of an Android application in terminal

通过总结,你可以快速了解每种类型(例如资源、原生代码、Flutter 库等)的大小使用情况。编译后的 Dart 原生库会按包进一步拆分,以便快速分析。

Through the summary, you can get a quick idea of the size usage per category (such as asset, native code, Flutter libraries, etc). The compiled Dart native library is further broken down by package for quick analysis.

在 DevTools 中深入分析

Deeper analysis in DevTools

上面生成的 *-code-size-analysis_*.json 文件可以在 DevTools 中进一步深入分析,树和树状图可以将应用内容分割至单文件级别,也可以达到 Dart AOT 产物的函数级别。

The *-code-size-analysis_*.json file produced above can be further analyzed in deeper detail in DevTools where a tree or a treemap view can break down the contents of the application into the individual file level and up to function level for the Dart AOT artifact.

可以通过 flutter pub global run devtools 打开 DevTools,选择 Open app size tool,然后上传 JSON 文件。

This can be done by flutter pub global run devtools, selecting Open app size tool and uploading the JSON file.

Example breakdown of app in DevTools

更多关于 DevTools 中应用大小工具的使用,请看 DevTools 文档

For further information on using the DevTools app size tool, see DevTools documentation.

减少应用大小

Reducing app size

当构建应用的发行版本时,考虑使用 --split-debug-info 标记。该标记会显著减少代码量。关于使用此标记的示例,请查看文档 Obfuscating Dart code

When building a release version of your app, consider using the --split-debug-info tag. This tag can dramatically reduce code size. For an example of using this tag, see Obfuscating Dart code.

其他减少应用大小的方式:

Some other things you can do to make your app smaller are:

延迟加载组件

目录

简介

Introduction

Flutter 支持构建在运行时下载额外 Dart 代码和静态资源的应用程序。这可以减少安装应用程序 apk 的大小,并在用户需要时下载功能和静态资源。

Flutter has the capability to build apps that can download additional Dart code and assets at runtime. This allows apps to reduce install apk size and download features and assets when needed by the user.

我们将每个独立的可下载的 Dart 库和静态资源称为「延迟组件」。这是通过使用 Dart 的延迟导入来实现的,可以将其编译到拆分的 AOT 共享库中。

We refer to each uniquely downloadable bundle of Dart libraries and assets as a “deferred component”. This is achieved by using Dart’s deferred imports, which can be compiled into split AOT shared libraries.

尽管模块可以延迟加载,但整个应用程序必须作为单个 App Bundle 完全构建和上传。不支持在没有重新上传整个新 Android 应用程序包的情况下发送部分更新。

Though modules can be defer loaded, the entire application must be completely built and uploaded as a single Android App Bundle. Dispatching partial updates without re-uploading new Android App Bundles for the entire application is not supported.

延迟加载仅在应用程序编译为 Release 或 Profile 模式 时可用。在 Debug 模式下,所有延迟组件都被视为常规导入,它们在启动时立即加载。因此,Debug 模式下仍然可以热重载。

Deferred loading is only performed when the app is compiled to release or profile mode. In debug mode, all deferred components are treated as regular imports, so they are present at launch and load immediately. Therefore, debug builds can still hot reload.

关于此功能的技术细节,请查看 Flutter wiki 上的 延迟加载组件

For a deeper dive into the technical details of how this feature works, see Deferred Components on the Flutter wiki.

如何让项目支持延迟加载组件

How to set your project up for deferred components

下面的引导将介绍如何设置 Android 应用程序以支持延迟加载。

The following instructions explain how to set up your Android app for deferred loading.

步骤 1:依赖项和初始项目设置

Step 1: Dependencies and initial project setup

  1. 将 Play Core 添加到 Android 应用程序的 build.gradle 依赖项中。在 android/app/build.gradle 中添加以下内容:

    Add Play Core to the Android app’s build.gradle dependencies. In android/app/build.gradle add the following:

    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    
  2. 如果使用 Google Play 商店作为动态功能的分发模型,应用程序必须支持 SplitCompat 并手动提供 PlayStoreDeferredComponentManager 的实例。这两个任务都可以通过设置 android/app/src/main/AndroidManifest.xml 中的 android:nameio.flutter.embedding.android.FlutterPlayStoreSplitApplication 应用属性来完成:

    If using the Google Play Store as the distribution model for dynamic features, the app must support SplitCompat and provide an instance of a PlayStoreDeferredComponentManager. Both of these tasks can be accomplished by setting the android:name property on the application in android/app/src/main/AndroidManifest.xml to io.flutter.embedding.android.FlutterPlayStoreSplitApplication:

    <manifest ...
      <application
         android:name="io.flutter.embedding.android.FlutterPlayStoreSplitApplication"
            ...
      </application>
    </manifest>
    

    io.flutter.app.FlutterPlayStoreSplitApplication 已经为你完成了这两项任务。如果你使用了 FlutterPlayStoreSplitApplication,可以跳转至步骤 1.3。

    io.flutter.app.FlutterPlayStoreSplitApplication handles both of these tasks for you. If you use FlutterPlayStoreSplitApplication, you can skip to step 1.3.

    如果你的 Android 应用程序很大或很复杂,你可能需要单独支持 SplitCompat 并提供 PlayStoreDynamicFeatureManager

    If your Android application is large or complex, you might want to separately support SplitCompat and provide the PlayStoreDynamicFeatureManager manually.

    要支持 SplitCompat,有三种方法(详见 Android docs),其中任何一种都是有效的:

    To support SplitCompat, there are three methods (as detailed in the Android docs), any of which are valid:

    • 让你的 application 类继承 SplitCompatApplication

      Make your application class extend SplitCompatApplication:

      public class MyApplication extends SplitCompatApplication {
          ...
      }
      
    • attachBaseContext() 中调用 SplitCompat.install(this);

      Call SplitCompat.install(this); in the attachBaseContext() method:

      @Override
      protected void attachBaseContext(Context base) {
          super.attachBaseContext(base);
          // Emulates installation of future on demand modules using SplitCompat.
          SplitCompat.install(this);
      }
      
    • SplitCompatApplication 声明为 application 的子类,并将 FlutterApplication 中的 flutter 兼容性代码添加到你的 application 类中:

      Declare SplitCompatApplication as the application subclass and add the Flutter compatibility code from FlutterApplication to your application class:

      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      

    嵌入层依赖注入的 DeferredComponentManager 实例来处理延迟组件的安装请求。通过在应用程序的初始流程中添加以下代码,将 PlayStoreDeferredComponentManager 添加到 Flutter 嵌入层中:

    The embedder relies on an injected DeferredComponentManager instance to handle install requests for deferred components. Provide a PlayStoreDeferredComponentManager into the Flutter embedder by adding the following code to your app initialization:

    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    import io.flutter.FlutterInjector;
    ... 
    PlayStoreDeferredComponentManager deferredComponentManager = new
      PlayStoreDeferredComponentManager(this, null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    
  3. 通过将 deferred-components 依赖添加到应用程序的 pubspec.yaml 中的 flutter 下,并选择延迟组件:

    Opt into deferred components by adding the deferred-components entry to the app’s pubspec.yaml under the flutter entry:

      ...
      flutter:
        ...
        deferred-components:
        ...
    

    flutter 工具会在 pubspec.yaml 中查找 deferred-components,来确定是否应将应用程序构建为延迟加载。除非你已经知道所需的组件和每个组件中的 Dart 延迟库,否则可以暂时将其留空。当 gen_snapshot 生成加载单元后,你可以在后面的 步骤 3.3 中完善这部分内容。

    The flutter tool looks for the deferred-components entry in the pubspec.yaml to determine whether the app should be built as deferred or not. This can be left empty for now unless you already know the components desired and the Dart deferred libraries that go into each. You will fill in this section later in step 3.3 once gen_snapshot produces the loading units.

步骤 2:实现延迟加载的 Dart 库

Step 2: Implementing deferred Dart libraries

接下来,在 Dart 代码中实现延迟加载的 Dart 库。实现并非立刻需要的功能。文章剩余部分中的示例添加了一个简单的延迟 widget 作为占位。你还可以通过修改 loadLibrary()Futures 后面的延迟加载代码的导入和保护用法,将现有代码转换为延迟代码。

Next, implement deferred loaded Dart libraries in your app’s Dart code. The implementation does not need to be feature complete yet. The example in the rest of this page adds a new simple deferred widget as a placeholder. You can also convert existing code to be deferred by modifying the imports and guarding usages of deferred code behind loadLibrary() Futures.

  1. 创建新的 Dart 库。例如,创建一个可以在运行时下载的 DeferredBox widget。这个 widget 可以是任意复杂的,本指南使用以下内容创建了一个简单的框。

    Create a new Dart library. For example, create a new DeferredBox widget that can be downloaded at runtime. This widget can be of any complexity but, for the purposes of this guide, create a simple box as a stand-in. To create a simple blue box widget, create box.dart with the following contents:

    // box.dart
    
    import 'package:flutter/widgets.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      DeferredBox() {}
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 30,
          width: 30,
          color: Colors.blue,
        );
      }
    }
    
  2. 在应用中使用 deferred 关键字导入新的 Dart 库,并调用 loadLibrary()(请参见 延迟加载库)。下面的示例使用 FutureBuilder 等待 loadLibraryFuture 对象(在 initState 中创建)完成,并将 CircularProgressIndicator 做为占位。当 Future 完成时,会返回 DeferredBoxSomeWidget 便可在应用程序中正常使用,在成功加载之前不会尝试访问延迟的 Dart 代码。

    Import the new Dart library with the deferred keyword in your app and call loadLibrary() (see lazily loading a library). The following example uses FutureBuilder to wait for the loadLibrary Future (created in initState) to complete and display a CircularProgressIndicator as a placeholder. When the Future completes, it returns the DeferredBox widget. SomeWidget can then be used in the app as normal and won’t ever attempt to access the deferred Dart code until it has successfully loaded.

    import 'box.dart' deferred as box;
    
    // ...
    
    class SomeWidget extends StatefulWidget {
      @override
      _SomeWidgetState createState() => _SomeWidgetState();
    }
    
    class _SomeWidgetState extends State<SomeWidget> {
      Future<void> _libraryFuture;
    
      @override
      void initState() {
        _libraryFuture = box.loadLibrary();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<void>(
          future: _libraryFuture,
          builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              }
              return box.DeferredBox();
            }
            return CircularProgressIndicator();
          },
        );
      }
    }
    // ...
    

    loadLibrary() 函数返回一个 Future<void> 对象,该对象会在延迟库中的代码可用时成功返回,否则返回一个错误。延迟库中所有的符号在使用之前都应确保 loadLibrary() 已经完成。所有导入的库都必须通过 deferred 标记,以便对其进行适当的编译以及在延迟组件中使用。如果组件已经被加载,再次调用 loadLibrary 将快速返回(但不是同步完成)。也可以提前调用 loadLibrary() 函数进行预加载,以帮助屏蔽加载时间。

    The loadLibrary() function returns a Future<void> that completes successfully when the code in the library is available for use and completes with an error otherwise. All usage of symbols from the deferred library should be guarded behind a completed loadLibrary() call. All imports of the library must be marked as deferred for it to be compiled appropriately to be used in a deferred component. If a component has already been loaded, additional calls to loadLibrary() complete quickly (but not synchronously). The loadLibrary() function can also be called early to trigger a pre-load to help mask the loading time.

    你可以在 Flutter Gallery’s lib/deferred_widget.dart 中找到其他延迟加载组件的示例。

    You can find another example of deferred import loading in Flutter Gallery’s lib/deferred_widget.dart.

步骤 3:构建应用程序

Step 3: Building the app

使用以下 flutter 命令构建延迟组件应用:

Use the following flutter command to build a deferred components app:

$ flutter build appbundle

此命令会帮助你检查项目是否正确设置为构建延迟组件应用。默认情况下,验证程序检测到任何问题都会导致构建失败,你可以通过系统建议的更改来修复这些问题。

This command assists you by validating that your project is properly set up to build deferred components apps. By default, the build fails if the validator detects any issues and guides you through suggested changes to fix them.

  1. flutter build appbundle 命令会尝试构建应用,通过 gen_snapshot 将应用中拆分的 AOT 共享库分割为单独的 .so 文件。第一次运行时,验证程序可能会在检测到问题时失败,该工具会为如何设置项目和解决这些问题提供建议。

    The flutter build appbundle command runs the validator and attempts to build the app with gen_snapshot instructed to produce split AOT shared libraries as separate .so files. On the first run, the validator will likely fail as it detects issues; the tool makes recommendations for how to set up the project and fix these issues.

    验证程序分为两个部分:预构建和生成快照后的验证。这是因为在 gen_snapshot 完成并生成最后一组加载单元之前,无法执行任何引用加载单元的验证。

    The validator is split into two sections: prebuild and post-gen_snapshot validation. This is because any validation referencing loading units cannot be performed until gen_snapshot completes and produces a final set of loading units.

    验证程序会检测 gen_snapshot 生成的所有新增、修改或者删除的加载单元。当前生成的加载单元记录在 <projectDirectory>/deferred_components_loading_units.yaml 文件中。这个文件应该加入到版本管理中,以确保其他开发人员对加载单元所做的更改可被追踪。

    The validator detects any new, changed, or removed loading units generated by gen_snapshot. The current generated loading units are tracked in your <projectDirectory>/deferred_components_loading_units.yaml file. This file should be checked into source control to ensure that changes to the loading units by other developers can be caught.

    验证程序还会检查 android 目录中的以下内容:

    The validator also checks for the following in the android directory:

    • 每个延迟组件名称的键值对映射 ${componentName}Name${componentName}。每个功能模块的 AndroidManifest.xml 使用此字符串资源来定义 dist:title property。例如:

      <projectDir>/android/app/src/main/res/values/strings.xml
      An entry for every deferred component mapping the key ${componentName}Name to ${componentName}. This string resource is used by the AndroidManifest.xml of each feature module to define the dist:title property. For example:

      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      
    • 每个延迟组件都有一个 Android 动态功能模块,它包含一个 build.gradlesrc/main/AndroidManifest.xml 文件。验证程序只检查文件是否存在,不验证文件内容。如果文件不存在,它将生成一个默认的推荐文件。

      <projectDir>/android/<componentName>
      An Android dynamic feature module for each deferred component exists and contains a build.gradle and src/main/AndroidManifest.xml file. This only checks for existence and does not validate the contents of these files. If a file does not exist, it generates a default recommended one.

    • 包含一个 meta-data 键值对,对加载单元与其关联的组件名称之间的映射进行编码。嵌入程序使用此映射将 Dart 的内部加载单元 id 转换为要安装的延迟组件的名称。例如:

      <projectDir>/android/app/src/main/res/values/AndroidManifest.xml
      Contains a meta-data entry that encodes the mapping between loading units and component name the loading unit is associated with. This mapping is used by the embedder to convert Dart’s internal loading unit id to the name of a deferred component to install. For example:

          ...
          <application
              android:label="MyApp"
              android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
              android:icon="@mipmap/ic_launcher">
              ...
              <meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="2:boxComponent"/>
          </application>
          ...
      

    gen_snapshot 验证程序在预构建验证通过之前不会运行。

    The gen_snapshot validator won’t run until the prebuild validator passes.

  2. 对于每个检查,该工具会创建或者修改需要的文件。这些文件放在 <projectDir>/build/android_deferred_components_setup_files 目录下。建议通过复制和覆盖项目 android 目录中的相同文件来应用更改。在覆盖之前,当前的项目状态应该被提交到源代码管理中,并检查建议的改动。该工具不会自动更改 android 目录。

    For each of these checks, the tool produces the modified or new files needed to pass the check. These files are placed in the <projectDir>/build/android_deferred_components_setup_files directory. It is recommended that the changes be applied by copying and overwriting the same files in the project’s android directory. Before overwriting, the current project state should be committed to source control and the recommended changes should be reviewed to be appropriate. The tool won’t make any changes to your android/ directory automatically.

  3. 一旦生成可用的加载单元并将其记录到 <projectDirectory>deferred_components_loading_units.yaml 中,便可完善 pubspec 的 deferred-components 配置,将加载单元分配给延迟的组件。在上面的案例中,生成的 deferred_components_loading_units.yaml 文件将包含:

    Once the available loading units are generated and logged in <projectDirectory>/deferred_components_loading_units.yaml, it is possible to fully configure the pubspec’s deferred-components section so that the loading units are assigned to deferred components as desired. To continue with the box example, the generated deferred_components_loading_units.yaml file would contain:

    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    

    加载单元 id(在本例中为「2」)由 Dart 内部使用,可以忽略。基本加载单元(id 为「1」)包含了其他加载单元中未显式列出的所有内容,在这里没有列出。

    The loading unit id (‘2’ in this case) is used internally by Dart, and can be ignored. The base loading unit (id ‘1’) is not listed and contains everything not explicitly contained in another loading unit.

    现在可以将以下内容添加到 pubspec.yaml 中:

    You can now add the following to pubspec.yaml:

    ...
    flutter:
      ...
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      ...
    

    将加载单元分配到延迟组件,把加载单元中的任何 Dart 库添加到功能模块的 libraries 部分。请记住以下准则:

    To assign a loading unit to a deferred component, add any Dart lib in the loading unit into the libraries section of the feature module. Keep the following guidelines in mind:

    • 一个加载单元只能包含在一个延迟组件中

      Loading units should not be included in more than one component.

    • 引用加载单元中的一个 Dart 库意味着整个加载单元都被包含在延迟组件中。

      Including one Dart library from a loading unit indicates that the entire loading unit is assigned to the deferred component.

    • 所有未被分配给延迟组件的加载单元都包含在基本组件中,基本组件始终隐式存在。

      All loading units not assigned to a deferred component are included in the base component, which always exists implicitly.

    • 分配给同一延迟组件的加载单元将一起下载、安装和运行。

      Loading units assigned to the same deferred component are downloaded, installed, and shipped together.

    • 基本组件是隐式的,不需要在 pubspec 中定义。

      The base component is implicit and need not be defined in the pubspec.

  4. 静态资源也可以通过在延迟组件中配置 assets 进行添加:

    Assets can also be included by adding an assets section in the deferred component configuration:

      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
          assets:
            - assets/image.jpg
            - assets/picture.png
              # wildcard directory
            - assets/gallery/
    

    一个静态资源可以包含在多个延迟组件中,但是安装这两个组件会导致资源的重复。也可以通过省略 libraries 来定义纯静态资源的延迟组件。这些静态资源的组件必须与服务中的 DeferredComponent 实用程序类一起安装,而不是 loadLibrary()。由于 Dart 库是与静态资源打包在一起的,因此如果用 loadLibrary() 加载 Dart 库,则也会加载组件中的所有资源。但是,按组件名称和服务实用程序来安装不会加载组件中的任何 Dart 库。

    An asset can be included in multiple deferred components, but installing both components results in a replicated asset. Assets-only components can also be defined by omitting the libraries section. These assets-only components must be installed with the DeferredComponent utility class in services rather than loadLibrary(). Since Dart libs are packaged together with assets, if a Dart library is loaded with loadLibrary(), any assets in the component are loaded as well. However, installing by component name and the services utility won’t load any dart libraries in the component.

    你可以自由选择将资源包含在任何组件中,只要它们是在首次引用时安装和加载的,但通常情况下,静态资源和使用这些资源的 Dart 代码最好打包在同一组件中。

    You are free to include assets in any component, as long as they are installed and loaded when they are first referenced, though typically, assets and the Dart code that uses those assets are best packed in the same component.

  5. 将在 pubspec.yaml 中定义的所有延迟组件手动添加到 android/settings.gradle 文件中的 includes 部分。例如,如果 pubspec 中定义了三个名为 boxComponentcircleComponentassetComponent 的延迟组件,请确保 android/settings.gradle 中包含以下内容:

    Manually add all deferred components that you defined in pubspec.yaml into the android/settings.gradle file as includes. For example, if there are three deferred components defined in the pubspec named, boxComponent, circleComponent, and assetComponent, ensure that android/settings.gradle contains the following:

    include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
    ...
    
  6. 重复步骤 3.1 到 3.6(此步骤),直到处理了所有验证程序的建议,并且该工具在没有更多建议的情况下运行。

    Repeat steps 3.1 through 3.6 (this step) until all validator recommendations are handled and the tool runs without further recommendations.

    成功时,此命令将在 build/app/outputs/bundle/release 目录下输出 app-release.aab 文件。

    When successful, this command outputs an app-release.aab file in build/app/outputs/bundle/release.

    构建成功并非总是意味着应用是按预期构建的。你需要确保所有的加载单元和 Dart 库都以你想要的方式包含在内。例如,一个常见的错误是不小心导入了一个没有 deferred 关键字的 Dart 库,导致一个延迟加载库被编译为基本加载单元的一部分。在这种情况下,Dart 库将正确加载,因为它始终存在于基本组件中,并且库不会被拆分。可以通过检查 deferred_components_loading_units.yaml 文件,验证预期的加载单元是否生成描述。

    A successful build does not always mean the app was built as intended. It is up to you to ensure that all loading units and Dart libraries are included in the way you intended. For example, a common mistake is accidentally importing a Dart library without the deferred keyword, resulting in a deferred library being compiled as part of the base loading unit. In this case, the Dart lib would load properly because it is always present in the base, and the lib would not be split off. This can be checked by examining the deferred_components_loading_units.yaml file to verify that the generated loading units are described as intended.

    当调整延迟组件配置,或者进行添加、修改、删除加载单元的更改时,你应该预料到验证程序会失败。按照步骤 3.1 到 3.6(此步骤)中的所有建议继续构建。

    When adjusting the deferred components configurations, or making Dart changes that add, modify, or remove loading units, you should expect the validator to fail. Follow steps 3.1 through 3.6 (this step) to apply any recommended changes to continue the build.

在本地运行应用

Running the app locally

一旦你的应用程序成功构建了一个 .aab 文件,就可以使用 Android 的 bundletool 来执行带有 --local testing 标志的本地测试。

Once your app has successfully built an .aab file, use Android’s bundletool to perform local testing with the --local-testing flag.

要在测试设备上运行 .aab 文件,请从 github.com/google/bundletool/releases 下载 bundletool jar 可执行文件,然后运行:

To run the .aab file on a test device, download the bundletool jar executable from github.com/google/bundletool/releases and run:

$ java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing

$ java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks

<your_app_project_dir> 是应用程序对应项目的目录位置, <your_temp_dir> 用于存储 bundletool 输出的所有临时目录。这会将你的 .aab 文件解压为 .apks 文件并将其安装到设备上。所有可用的 Android 动态特性都已在本地设备上加载,并模拟了延迟组件的安装。

Where <your_app_project_dir> is the path to your app’s project directory and <your_temp_dir> is any temporary directory used to store the outputs of bundletool. This unpacks your .aab file into an .apks file and installs it on the device. All available Android dynamic features are loaded onto the device locally and installation of deferred components is emulated.

再次运行 build-apks 之前,请删除已存在的 .apks 文件:

Before running build-apks again, remove the existing app .apks file:

$ rm <your_temp_dir>/app.apks

对 Dart 代码库的更改需要增加 Android 构建 ID,或者卸载并重新安装应用程序。因为只有检测到新的版本号,Android 才会去更新功能模块。

Changes to the Dart codebase require either incrementing the Android build ID or uninstalling and reinstalling the app, as Android won’t update the feature modules unless it detects a new version number.

发布到 Google Play 商店

Releasing to the Google Play store

生成的 .aab 文件可以像平常一样直接上传到 Google Play 商店。调用 loadLibrary() 时,Flutter 引擎将会使用从商店下载的包含 Dart AOT 库和资源的 Android 模块。

The built .aab file can be uploaded directly to the Play store as normal. When loadLibrary() is called, the needed Android module containing the Dart AOT lib and assets is downloaded by the Flutter engine using the Play store’s delivery feature.

提高渲染性能

目录

在衡量性能时,应用程序中的渲染动画一直是最受关注的话题之一。由于 Flutter 自带的 Skia 引擎以及它能够快速创建和处理组件的能力, Flutter 应用在默认情况下就能保证拥有良好的性能,因此我们只需避开常见的陷阱就可以获得出色的性能。

Rendering animations in your app is one of the most cited topics of interest when it comes to measuring performance. Thanks in part to Flutter’s Skia engine and its ability to quickly create and dispose of widgets, Flutter applications are performant by default, so you only need to avoid common pitfalls to achieve excellent performance.

一些基本的建议

General advice

如果看到不稳定(不流畅)的动画,请 确保 你正在做性能分析的应用是在 profile 模式下构建的,因为默认情况下 Flutter 会在 debug 模式下创建应用,这并不表示应用正式发布后的性能。更多信息,参见 Flutter 的构建模式

If you are seeing janky (non smooth) animations, make sure that you are profiling performance with an app built in profile mode. The default Flutter build creates an app in debug mode, which is not indicative of release performance. For information, see Flutter’s build modes.

有几种常见的陷阱:

A couple common pitfalls:

有关评估性能的更多资料(包括常见缺陷),请参阅以下文档:

For more information on evaluating performance including information on common pitfalls, see the following docs:

纯移动应用

Mobile-only advice

如果移动应用里遇到一些肉眼可见的卡顿,单只是在第一次运行动画的时候?如果是这样的话,可以查看这个文档 减少过移动应用的着色器动画卡顿

Do you see noticeable jank on your mobile app, but only on the first run of an animation? If so, see Reduce shader animation jank on mobile.

纯 Web 应用

Web-only advice

下面的内容是 Flutter Material 团队在提高 Flutter Gallery Web 应用性能时候总结的经验:

The following series of articles cover what the Flutter Material team learned when improving performance of the Flutter Gallery app on the web:

Flutter 应用性能优化最佳实践

目录

通常来说,Flutter 构建的应用程序在默认情况下都是高性能的。所以你只需要避开常见的陷阱,不需要使用复杂的分析工具对细节做优化,就可以获得优异的性能,这些最佳建议将帮助你编写性能最佳的 Flutter 应用程序。

Generally, Flutter applications are performant by default, so you only need to avoid common pitfalls to get excellent performance. These best recommendations will help you write the most performant Flutter app possible.

如果你在用 Flutter 编写 Web 应用,你可能会对下面的系列文章感兴趣,他们由 Flutter Material 团队撰写,记录了对 Flutter Gallery 应用的修改,使其在 Web 上的性能更好:

If you are writing web apps in Flutter, you might be interested in a series of articles, written by the Flutter Material team, after they modified the Flutter Gallery app to make it more performant on the web:

最佳实践

Best practices

如何设计一个能最有效地渲染页面的 Flutter 应用程序?特别是如何确保底层框架生成的绘图代码尽可能高效?这里有几件需要你在设计应用时考虑的事情:

How do you design a Flutter app to most efficiently render your scenes? In particular, how do you ensure that the painting code generated by the framework is as efficient as possible? Here are a few things to consider when designing your app:

控制 build() 方法的耗时

Controlling build() cost

另请参考:

Also see:

仅当需要的时候才应用效果

Apply effects only when needed

由于代价很大,请谨慎使用效果。一些效果的背后调用了性能代价很大的 saveLayer() 方法。

Use effects carefully, as they can be expensive. Some of them invoke saveLayer() behind the scenes, which can be an expensive operation.

一些在使用效果时的通用规则:

Some general rules when applying specific effects:

其他会触发 saveLayer() 的 widget,可能也会代价高昂。

Other widgets that might trigger saveLayer() and are potentially costly:

避免调用 saveLayer() 的方式:

Ways to avoid calls to saveLayer():

对列表和网格列表懒加载

Render grids and lists lazily

在构建大型网格或列表时,使用带有回调的惰性方法。这样,只有屏幕的可见部分是在开始时构建的。

Use the lazy methods, with callbacks, when building large grids or lists. That way only the visible portion of the screen is built at startup time.

请参阅:

Also see:

在 16ms 内渲染完成每一帧

Build and display frames in 16ms

由于构建和渲染有两个独立的线程,因此构建时间为 16ms,60Hz 显示器上渲染时间为 16ms。如果需要考虑延迟,就要在 16ms 或更短 的时间内构建和显示帧。请注意,这意味着构建需要少于 8ms,渲染也需要少于 8ms,总计 16ms 或更短。如果需要考虑丢帧(jankyness),那么每个构建和渲染阶段的 16ms 都可以。

Since there are two separate threads for building and rendering, you have 16ms for building, and 16ms for rendering on a 60Hz display. If latency is a concern, build and display a frame in 16ms or less. Note that means built in 8ms or less, and rendered in 8ms or less, for a total of 16ms or less. If missing frames (jankyness) is a concern, then 16ms for each of the build and render stages is OK.

如果在 profile 构建 状态下,每一帧渲染时间低于 16ms,你可能不必担心性能问题以及一些性能陷阱,但仍然应该致力于尽可能快地渲染每一帧。为什么?

If your frames are rendering in well under 16ms total in profile mode, you likely don’t have to worry about performance even if some performance pitfalls apply, but you should still aim to build and render a frame as fast as possible. Why?

如果你想弄明白为什么 60fps 会带来平滑的视觉体验,请看视频 Why 60fps?

If you are wondering why 60fps leads to a smooth visual experience, see the video Why 60fps?

陷阱

Pitfalls

如果你需要改善应用程序的性能,或者 UI 流畅度没达到你的预期,那么 IDE 的 Flutter plugin 可以提供帮助。在 Flutter Performance 窗口中,勾选 Show widget rebuild information 复选框。此功能可帮助你检测帧的渲染和显示时间是否超过 16ms。在可能的情况下,插件也会提供指向相关提示的链接。

If you need to tune your app’s performance, or perhaps the UI isn’t as smooth as you expect, the Flutter plugin for your IDE can help. In the Flutter Performance window, enable the Show widget rebuild information check box. This feature helps you detect when frames are being rendered and displayed in more than 16ms. Where possible, the plugin provides a link to a relevant tip.

以下行为可能会对您应用的性能产生负面影响。

The following behaviors might negatively impact your app’s performance.

参考资料

Resources

要了解更多性能信息,请参见以下资源:

For more performance info, see the following resources:

Flutter 性能分析

目录

有句话叫「的应用固然很好,但流畅的应用则更好。」如果你的应用渲染并不流畅,该怎么处理呢?从哪里着手呢?本文展示了应该从哪里着手,步骤以及可以提供帮助的工具。

It’s been said that “a fast app is great, but a smooth app is even better.” If your app isn’t rendering smoothly, how do you fix it? Where do you begin? This guide shows you where to start, steps to take, and tools that can help.

分析性能问题

Diagnosing performance problems

分析应用的性能问题需要打开性能监控图层 (performance overlay) 来观察 UI 和 GPU 线程。在此之前,要确保是在 分析模式 下运行,而且当前设备不是虚拟机。使用用户可能采用的最慢设备来获取最佳结果。

To diagnose an app with performance problems, you’ll enable the performance overlay to look at the UI and raster threads. (The raster thread was previously known as the GPU thread.) Before you begin, you want to make sure that you’re running in profile mode, and that you’re not using an emulator. For best results, you might choose the slowest device that your users might use.

连接到物理设备

Connect to a physical device

几乎全部的 Flutter 应用性能调试都应该在真实的 Android 或者 iOS 设备上以 分析模式 进行。通常来说,调试模式或者是模拟器上运行的应用的性能指标和发布模式的表现并不相同。 应该考虑在用户使用的最慢的设备上检查性能。

Almost all performance debugging for Flutter applications should be conducted on a physical Android or iOS device, with your Flutter application running in profile mode. Using debug mode, or running apps on simulators or emulators, is generally not indicative of the final behavior of release mode builds. You should consider checking performance on the slowest device that your users might reasonably use.

在分析模式运行

Run in profile mode

除了一些调试性能问题所必须的额外方法, Flutter 的分析模式和发布模式的编译和运行基本相同。例如,分析模式为分析工具提供了追踪信息。

Flutter’s profile mode compiles and launches your application almost identically to release mode, but with just enough additional functionality to allow debugging performance problems. For example, profile mode provides tracing information to the profiling tools.

使用分析模式运行应用的方法:

Launch the app in profile mode as follows:

关于不同模式的更多信息,请参考文档: Flutter 的构建模式选择

For more information on the different modes, see Flutter’s build modes.

下面我们会从打开 DevTools、查看性能图层开始讲述。

You’ll begin by opening DevTools and viewing the performance overlay, as discussed in the next section.

运行 DevTools

Launch DevTools

Dart DevTool 提供诸如性能分析、堆测试以及显示代码覆盖率等功能。 DevTool 的 [Timeline] 界面可以让开发者逐帧分析应用的 UI 性能。

DevTools provides features like profiling, examining the heap, displaying code coverage, enabling the performance overlay, and a step-by-step debugger. DevTools’ Timeline view allows you to investigate the UI performance of your application on a frame-by-frame basis.

一旦你的应用程序在分析模式下运行,即 运行 DevTools

Once your app is running in profile mode, launch DevTools.

性能图层

The performance overlay

性能图层用两张图表显示应用的耗时信息。如果 UI 产生了卡顿(跳帧),这些图表可以帮助分析原因。图表在当前应用的最上层展示,但并不是用普通的 widget 方式绘制的—Flutter 引擎自身绘制了该图层来尽可能减少对性能的影响。每一张图表都代表当前线程的最近 300 帧表现。

The performance overlay displays statistics in two graphs that show where time is being spent in your app. If the UI is janky (skipping frames), these graphs help you figure out why. The graphs display on top of your running app, but they aren’t drawn like a normal widget—the Flutter engine itself paints the overlay and only minimally impacts performance. Each graph represents the last 300 frames for that thread.

本节阐述如何打开性能图层并用其来分析应用中卡顿的原因。下面的截图展示了 Flutter Gallery 样例的性能图层:

This section describes how to enable the performance overlay and use it to diagnose the cause of jank in your application. The following screenshot shows the performance overlay running on the Flutter Gallery example:

Screenshot of overlay showing zero jank


raster 线程的性能情况在上面,UI 线程显示在下面。
垂直的绿色条条代表的是当前帧。


Performance overlay showing the raster thread (top), and UI thread (bottom).
The vertical green bars represent the current frame.

图表解释

Interpreting the graphs

最顶部(标志了 “GPU”)的图形表示 raster 线程所花费的时间,底部的图表显示了 UI 线程所花费的时间。横跨图表中的白线代表了 16 ms 内沿竖轴的增量;如果这些线在图表中都没有超过它的话,说明你的运行帧率低于 60 Hz。而横轴则表示帧。只有当你的应用绘制时这个图表才会更新,所以如果它空闲的话,图表就不会动。

The top graph (marked “GPU”) shows the time spent by the raster thread, the bottom one graph shows the time spent by the UI thread. The white lines across the graphs show 16ms increments along the vertical axis; if the graph ever goes over one of these lines then you are running at less than 60Hz. The horizontal axis represents frames. The graph is only updated when your application paints, so if it’s idle the graph stops moving.

这个浮层只应在 分析模式 中使用,因为在 调试模式 下有意牺牲了性能来换取昂贵的断言以帮助开发,所以这时候的结果会有误导性。

The overlay should always be viewed in profile mode, since debug mode performance is intentionally sacrificed in exchange for expensive asserts that are intended to aid development, and thus the results are misleading.

每一帧都应该在 1/60 秒(大约 16 ms)内创建并显示。如果有一帧超时(任意图像)而无法显示,就导致了卡顿,图表之一就会展示出来一个红色竖条。如果是在 UI 图表出现了红色竖条,则表明 Dart 代码消耗了大量资源。而如果红色竖条是在 GPU 图表出现的,意味着场景太复杂导致无法快速渲染。

Each frame should be created and displayed within 1/60th of a second (approximately 16ms). A frame exceeding this limit (in either graph) fails to display, resulting in jank, and a vertical red bar appears in one or both of the graphs. If a red bar appears in the UI graph, the Dart code is too expensive. If a red vertical bar appears in the GPU graph, the scene is too complicated to render quickly.

Screenshot of performance overlay showing jank with red bars


红色竖条表明当前帧的渲染和绘制都很耗时。
当两张图表都是红色时,就要开始对 UI 线程 (Dart VM) 进行诊断了。


The vertical red bars indicate that the current frame is expensive to both render and paint.
When both graphs display red, start by diagnosing the UI thread.

Flutter 的线程

Flutter’s threads

Flutter 使用多个线程来完成其必要的工作,图层中仅展示了其中两个线程。您写的所有 Dart 代码都在 UI 线程上运行。尽管您没有直接访问其他线程的权限,但是您对 UI 线程的操作会对其他线程产生性能影响。

Flutter uses several threads to do its work, though only two of the threads are shown in the overlay. All of your Dart code runs on the UI thread. Although you have no direct access to any other thread, your actions on the UI thread have performance consequences on other threads.

平台线程

Platform thread

平台线程实际上就是主线程。Plugin 的代码将会在这里运行。想要了解更多信息,请参阅 Android 的 MainThread 以及 iOS 的 UIKit 文档。

The platform’s main thread. Plugin code runs here. For more information, see the UIKit documentation for iOS, or the MainThread documentation for Android. This thread is not shown in the performance overlay.

UI 线程

UI thread

UI 线程在 Dart VM 中执行 Dart 代码。该线程包括开发者写下的代码和 Flutter 框架根据应用行为生成的代码。当应用创建和展示场景的时候,UI 线程首先建立一个 图层树(layer tree) ,一个包含设备无关的渲染命令的轻量对象,并将图层树发送到 GPU 线程来渲染到设备上。 不要阻塞这个线程! 在性能图层的最低栏展示该线程。

The UI thread executes Dart code in the Dart VM. This thread includes code that you wrote, and code executed by Flutter’s framework on your app’s behalf. When your app creates and displays a scene, the UI thread creates a layer tree, a lightweight object containing device-agnostic painting commands, and sends the layer tree to the raster thread to be rendered on the device. Don’t block this thread! Shown in the bottom row of the performance overlay.

Raster 线程(以前叫 GPU 线程)

Raster thread (previously known as the GPU thread)

raster 线程拿到 layer tree,并将它交给 GPU(图形处理单元)。你无法直接与 GPU 线程或其数据通信,但如果该线程变慢,一定是开发者 Dart 代码中的某处导致的。图形库 Skia 在该线程运行,并在性能图层的最顶栏显示该线程。这个线程之前被叫做「GPU 线程」,因为它为 GPU 进行栅格化,但我们重新将它命名为「raster 线程」,这是因为许多开发者错误的(但是能理解)认为该线程运行在 GPU 单元。

The raster thread takes the layer tree and displays it by talking to the GPU (graphic processing unit). You cannot directly access the raster thread or its data but, if this thread is slow, it’s a result of something you’ve done in the Dart code. Skia, the graphics library, runs on this thread. Shown in the top row of the performance overlay. This thread was previously known as the “GPU thread” because it rasterizes for the GPU. But it is running on the CPU. We renamed it to “raster thread” because many developers wrongly (but understandably) assumed the thread runs on the GPU unit.

I/O线程

I/O thread

执行昂贵的操作(常见的有 I/O)以避免阻塞 UI 或者 raster 线程。这个线程将不会显示在 performance overlay 上。

Performs expensive tasks (mostly I/O) that would otherwise block either the UI or raster threads. This thread is not shown in the performance overlay.

你可以在 GitHub wiki 上的框架结构 (The Framework architecture) 一文中了解更多信息和一些视频内容,另外你可以在我们的社区中查看文章 The Layer Cake

For links to more information and videos, see The Framework architecture on the GitHub wiki, and the community article, The Layer Cake.

显示性能图层

Displaying the performance overlay

你可以用如下方法显示性能图层:

You can toggle display of the performance overlay as follows:

使用 Flutter inspector

Using the Flutter inspector

打开 PerformanceOverlay widget 最简单的方法是 IDE 中 Flutter 插件提供的 Flutter inspector,你可以在 开发者工具使用 Flutter inspector 工具 中找到。只需单击 Performance Overlay 按钮,即可在正在运行的应用程序上切换图层。

The easiest way to enable the PerformanceOverlay widget is from the Flutter inspector, which is available in the Inspector view in DevTools. Simply click the Performance Overlay button to toggle the overlay on your running app.

命令行

From the command line

使用 P 参数触发性能图层。

Toggle the performance overlay using the P key from the command line.

代码控制

Programmatically

若要以编程的方式启用性能图层,请参考 以编程方式调试应用 文档的 性能图层 章节。

To enable the overlay programmatically, see Performance overlay, a section in the Debugging Flutter apps programmatically page.

定位 UI 图表中的问题

Identifying problems in the UI graph

如果性能图层的 UI 图表显示红色,就要从分析 Dart VM 开始着手了,即使 GPU 图表同样显示红色。

If the performance overlay shows red in the UI graph, start by profiling the Dart VM, even if the GPU graph also shows red.

定位 GPU 图表中的问题

Identifying problems in the GPU graph

有些情况下界面的图层树构造起来虽然容易,但在 raster 线程下渲染却很耗时。这种情况发生时,UI 图表没有红色,但 GPU 图表会显示红色。这时需要找出代码中导致渲染缓慢的原因。特定类型的负载对 GPU 来说会更加复杂。可能包括不必要的对 saveLayer 的调用,许多对象间的复杂操作,还可能是特定情形下的裁剪或者阴影。

Sometimes a scene results in a layer tree that is easy to construct, but expensive to render on the raster thread. When this happens, the UI graph has no red, but the GPU graph shows red. In this case, you’ll need to figure out what your code is doing that is causing rendering code to be slow. Specific kinds of workloads are more difficult for the GPU. They might involve unnecessary calls to saveLayer, intersecting opacities with multiple objects, and clips or shadows in specific situations.

如果推断的原因是动画中的卡顿的话,可以点击 Flutter inspector 中的 Slow Animations 按钮,来使动画速度减慢 5 倍。如果你想从更多方面控制动画速度,你可以参考 programmatically

If you suspect that the source of the slowness is during an animation, click the Slow Animations button in the Flutter inspector to slow animations down by 5x. If you want more control on the speed, you can also do this programmatically.

卡顿是第一帧发生的还是贯穿整个动画过程呢?如果是整个动画过程的话,会是裁剪导致的吗?也许有可以替代裁剪的方法来绘制场景。比如说,不透明图层的长方形中用尖角来取代圆角裁剪。如果是一个静态场景的淡入、旋转或者其他操作,可以尝试使用重绘边界 (RepaintBoundary)。

Is the slowness on the first frame, or on the whole animation? If it’s the whole animation, is clipping causing the slow down? Maybe there’s an alternative way of drawing the scene that doesn’t use clipping. For example, overlay opaque corners onto a square instead of clipping to a rounded rectangle. If it’s a static scene that’s being faded, rotated, or otherwise manipulated, a RepaintBoundary might help.

检查屏幕之外的视图

Checking for offscreen layers

保存图层 (saveLayer) 方法是 Flutter 框架中最重量的操作之一。更新屏幕时这个方法很有用,但它可能使应用变慢,如果不是必须的话,应该避免使用这个方法。即便没有显式地调用 saveLayer,也可能在其他操作中间接调用了该方法。可以使用棋盘画面以外的层 (PerformanceOverlayLayer.checkerboardOffscreenLayers) 开关来检查场景是否使用了 saveLayer

The saveLayer method is one of the most expensive methods in the Flutter framework. It’s useful when applying post-processing to the scene, but it can slow your app and should be avoided if you don’t need it. Even if you don’t call saveLayer explicitly, implicit calls might happen on your behalf. You can check whether your scene is using saveLayer with the PerformanceOverlayLayer.checkerboardOffscreenLayers switch.

打开开关之后,运行应用并检查是否有图像的轮廓闪烁。如果有新的帧渲染的话,容器就会闪烁。举个例子,也许有一组对象的透明度要使用 saveLayer 来渲染。在这种情况下,相比通过 widget 树中高层次的父 widget 操作,单独对每个 widget 来应用透明度可能性能会更好。其他可能大量消耗资源的操作也同理,比如裁剪或者阴影。

Once the switch is enabled, run the app and look for any images that are outlined with a flickering box. The box flickers from frame to frame if a new frame is being rendered. For example, perhaps you have a group of objects with opacities that are rendered using saveLayer. In this case, it’s probably more performant to apply an opacity to each individual widget, rather than a parent widget higher up in the widget tree. The same goes for other potentially expensive operations, such as clipping or shadows.

当遇到对 saveLayer 的调用时,先问问自己:

When you encounter calls to saveLayer, ask yourself these questions:

检查没有缓存的图像

Checking for non-cached images

使用重绘边界 (RepaintBoundary) 来缓存图片是个好主意,当需要的时候。

Caching an image with RepaintBoundary is good, when it makes sense.

从资源的角度看,最重量级的操作之一是用图像文件来渲染纹理。首先,需要从持久存储中取出压缩图像,然后解压缩到宿主存储中(GPU 存储),再传输到设备存储器中 (RAM) 。

One of the most expensive operations, from a resource perspective, is rendering a texture using an image file. First, the compressed image is fetched from persistent storage. The image is decompressed into host memory (GPU memory), and transferred to device memory (RAM).

也就是说,图像的 I/O 操作是重量级的。缓存提供了复杂层次的快照,这样就可以方便地渲染到随后的帧中。 因为光栅缓存入口的构建需要大量资源,同时增加了 GPU 存储的负载,所以只在必须时才缓存图片。

In other words, image I/O can be expensive. The cache provides snapshots of complex hierarchies so they are easier to render in subsequent frames. Because raster cache entries are expensive to construct and take up loads of GPU memory, cache images only where absolutely necessary.

打开覆盖层性能棋盘格光栅缓存图像 (PerformanceOverlayLayer.checkerboardRasterCacheImages) 开关可以检查哪些图片被缓存了。

You can see which images are being cached by enabling the PerformanceOverlayLayer.checkerboardRasterCacheImages switch.

运行应用来查看使用随机颜色网格渲染的图像,标识被缓存的图像。当和场景交互时,网格里的图片应该是静止的—代表重新缓存图片的闪烁视图不应该出现。

Run the app and look for images rendered with a randomly colored checkerboard, indicating that the image is cached. As you interact with the scene, the checkerboarded images should remain constant—you don’t want to see flickering, which would indicate that the cached image is being re-cached.

大多数情况下,开发者都希望在网格里看到的是静态图片,而不是非静态图片。如果静态图片没有被缓存,可以将其放到重绘边界 (RepaintBoundary) widget 中来缓存。虽然引擎也可能忽略 repaint boundary,如果它认为图像还不够复杂的话。

In most cases, you want to see checkerboards on static images, but not on non-static images. If a static image isn’t cached, you can cache it by placing it into a RepaintBoundary widget. Though the engine might still ignore a repaint boundary if it thinks the image isn’t complex enough.

检视 widget 重建性能

Viewing the widget rebuild profiler

Flutter 框架的设计使得构建达不到 60 fps 流畅度的应用变得困难。通常情况下如果卡顿,就是因为每一帧被重建的 UI 比需求更多的简单 bug。 Widget rebuild profiler 可以帮助调试和修复这些问题引起的 bug。

The Flutter framework is designed to make it hard to create applications that are not 60fps and smooth. Often, if you have jank, it’s because there is a simple bug causing more of the UI to be rebuilt each frame than required. The Widget rebuild profiler helps you debug and fix performance problems due to these sorts of bugs.

可以检视 widget inspector 中当前屏幕和帧下的 widget 重建数量。了解细节,可以参考 在 Android Studio 或类 IntelliJ 里开发 Flutter 应用 中的 显示性能数据

You can view the widget rebuilt counts for the current screen and frame in the Flutter plugin for Android Studio and IntelliJ. For details on how to do this, see Show performance data.

评分

Benchmarking

可以通过编写评分测试来测量和追踪应用的性能。 Flutter Driver 库提供了对评分的支持。基于这套测试框架就可以生成以下几项的测试标准:

You can measure and track your app’s performance by writing benchmark tests. The Flutter Driver library provides support for benchmarking. Using this integration test framework, you can generate metrics to track the following:

追踪这些评分可以在回归测试中了解对性能的不利影响。

Tracking these benchmarks allows you to be informed when a regression is introduced that adversely affects performance.

了解更多,请参考 测试 Flutter 应用 中的 集成测试 一节。

For more information, see Integration testing, a section in Testing Flutter apps.

更多资源

Other resources

以下链接提供了关于 Flutter 工具的使用和 Flutter 调试的更多信息:

The following resources provide more information on using Flutter’s tools and debugging in Flutter:

在移动设备上减少着色器编译卡顿

目录

如果在你的手机应用中发现有些动画出现了卡顿,但仅仅会在第一次运行的时候会有这种情况,那么你可以通过 Skia 的着色器语言进行着色器预热,带来颇见成效的改善。

If the animations on your mobile app appear to be janky, but only on the first run, you can warm up the shader captured in the Skia Shader Language (SkSL) for a significant improvement.

Side-by-side screenshots of janky mobile app next to non-janky app

什么是着色器编译卡顿?

What is shader compilation jank?

如果应用中一些动画在首次运行时出现卡顿,但同样的动画在之后变得流畅,那么这非常可能是由于着色器编译导致的卡顿。

If an app has janky animations during the first run, and later becomes smooth for the same animation, then it’s very likely due to shader compilation jank.

严格来说,着色器是运行在 GPU(图形处理单元)的一段代码。当首次使用着色器时,它需要在设备上进行编译。这个编译过程可能会消耗数百毫秒的时间,而要达到 60 fps 的流畅度,我们必须要在 16 毫秒以内绘制完一帧。因此,这个编译过程可能导致数十帧被丢失,让刷新率从 60 帧跌至 6 帧。这就是编译卡顿。在编译完成后,动画就流畅了。

More technically, a shader is a piece of code that runs on a GPU (graphics processing unit). When a shader is first used, it needs to be compiled on the device. The compilation could cost up to a few hundred milliseconds whereas a smooth frame needs to be drawn within 16 milliseconds for a 60 fps (frame-per-second) display. Therefore, a compilation could cause tens of frames to be missed, and drop the fps from 60 to 6. This is compilation jank. After the compilation is complete, the animation should be smooth.

要获得更加确切的着色器编译卡顿存在的证据,你可以在 --trace-skia 开启时查看追踪文件中的 GrGLProgramBuilder::finalize。下面的截图展示了一个 timeline 追踪的样例。

Definitive evidence for the presence of shader compilation jank is to see GrGLProgramBuilder::finalize in the tracing with --trace-skia enabled. See the following screenshot for an example timeline tracing.

A tracing screenshot verifying jank

如何定义「首次运行」?

What do we mean by “first run”?

在 Android 上来说,「首次运行」意味着用户可能在新安装应用后,第一次打开应用的时候看到了卡顿。而之后运行都很正常。

On Android, “first run” means that the user might see jank the first time opening the app after a fresh installation. Subsequent runs should be fine.

在 iOS 上来说,「首次运行」意味着用户可能在每次打开应用后,在动画首次加载时都会出现卡顿。

On iOS, “first run” means that the user might see jank when an animation first occurs every time the user opens the app from scratch.

如何使用 SkSL 预热

How to use SkSL warmup

在 1.20 发布的时候,Flutter 为应用开发者提供了一个命令行工具以收集终端用户在 SkSL(Skia 着色器语言)进行格式化处理中需要用到的着色器。 SkSL 着色器可以被打包进应用,并提前进行预热(预编译),这样当终端用户第一次打开应用时,就能够减少动画的编译掉帧了。使用下面的指令收集并打包 SkSL 的着色器:

As of release 1.20, Flutter provides command line tools for app developers to collect shaders that might be needed for end-users in the SkSL (Skia Shader Language) format. The SkSL shaders can then be packaged into the app, and get warmed up (pre-compiled) when an end-user first opens the app, thereby reducing the compilation jank in later animations. Use the following instructions to collect and package the SkSL shaders:

  1. ​ 打开 --cache-sksl 运行你的应用以捕获 SkSL 中的着色器:

    Run the app with --cache-sksl turned on to capture shaders in SkSL:

    flutter run --profile --cache-sksl
    

    如果这个相同的应用之前运行的时候没有使用 --cache-sksl,你需要加上 --purge-persistent-cache 标志:

    If the same app has been previously run without --cache-sksl, then the --purge-persistent-cache flag may be needed:

    flutter run --profile --cache-sksl --purge-persistent-cache
    

    这个标志将会删除可能干扰 SkSL 的较旧的非 SkSL 着色器缓存捕获的着色器。它还清除了 SkSL 着色器,因此在第一次使用 --cache-sksl 运行。

    This flag removes older non-SkSL shader caches that could interfere with SkSL shader capturing. It also purges the SkSL shaders so use it only on the first --cache-sksl run.

  2. 尽可能多触发应用的动画,特别是那些会引起编译卡顿的。

    Play with the app to trigger as many animations as needed; particularly those with compilation jank.

  3. 在执行 flutter run 命令后行按下 M 键以捕获 SkSL 着色器到一个类似 flutter_01.sksl.json 的文件中。最好在 Android 和 iOS 真机上分别抓取 SkSL 着色器。

    Press M at the command line of flutter run to write the captured SkSL shaders into a file named something like flutter_01.sksl.json. For best results, capture SkSL shaders on actual Android and iOS devices separately.

  4. ​ 在下面的命令中选择合适的构建带有 SkSL 预热的应用:

    Build the app with SkSL warm-up using the following, as appropriate:

    Android:

    flutter build apk --bundle-sksl-path flutter_01.sksl.json
    

    or

    flutter build appbundle --bundle-sksl-path flutter_01.sksl.json
    

    iOS:

    flutter build ios --bundle-sksl-path flutter_01.sksl.json
    

    如果它会构建一个类似 test_driver/app.dart 的驱动测试,请确保指定 --target=test_driver/app.dart。(例如 flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart

    If it’s built for a driver test like test_driver/app.dart, make sure to also specify --target=test_driver/app.dart (e.g., flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart).

  5. Test the newly built app.

或者,你可以编写一些集成测试来使用一个命令自动执行前三个步骤。例如:

Alternatively, you can write some integration tests to automate the first three steps using a single command. For example:

flutter drive --profile --cache-sksl --write-sksl-on-exit flutter_01.sksl.json -t test_driver/app.dart

使用这样的 集成测试,无论是代码发生改变,或者 Flutter 更新了,你都可以轻松获得可靠的着色器缓存。这些测试也被用于验证开启着色器预热前后的性能变化上。更好的做法是,你可以把这些测试放进 CI(持续集成)系统上,这样就能在每次应用发布前自动生成并测试着色器缓存了。

With such integration tests, you can easily and reliably get the new SkSLs when the app code changes, or when Flutter upgrades. Such tests can also be used to verify the performance change before and after the SkSL warm-up. Even better, you can put those tests into a CI (continuous integration) system so the SkSLs are generated and tested automatically over the lifetime of an app.

就拿原始版本的 Flutter Gallery 举例。我们让 CI 系统在每次 Flutter commit 后都生成着色器缓存,并在 transitions_perf_test.dart][] 中验证性能。更多详细信息请查看 Flutter Gallery sksl 预热过渡性能验证,以及 Flutter Gallery sksl 预热过渡在 iOS_32 上的性能验证

Take the original version of Flutter Gallery as an example. The CI system is set up to generate SkSLs for every Flutter commit, and verifies the performance, in the transitions_perf_test.dart test. For more details, see the flutter_gallery_sksl_warmup__transition_perf and flutter_gallery_sksl_warmup__transition_perf_e2e_ios32 tasks.

在这种这种集成测试中,最差的帧光栅化时间是一个很好的指标来衡量着色器编译卡顿的严重性。例如,上述步骤减少了 Flutter gallery 应用的着色器编译卡顿,并减少了它在 Moto G4 手机上的最差的帧光栅化时间,从 ~90 ms 减少到 ~40 ms。在 iPhone 4s 上,它从 ~300 ms 减少到 ~80 ms。这种视觉差异如同本文开头所示一样。

The worst frame rasterization time is a nice metric from such integration tests to indicate the severity of shader compilation jank. For instance, the steps above reduce Flutter gallery’s shader compilation jank and speeds up its worst frame rasterization time on a Moto G4 from ~90 ms to ~40 ms. On iPhone 4s, it’s reduced from ~300 ms to ~80 ms. That leads to the visual difference as illustrated in the beginning of this article.

常见问题

Frequently asked questions

  1. 为什么不把全部可能的着色器都给预编译/预热了?

    如果只有少数几个可能有的着色器的话, Flutter 当然可以在应用构建时将他们全部预编译。然而,对于最佳总体性能来看, Skia GPU 后端在运行时使用了 Flutter 基于许多参数(例如,绘图、设备型号和驱动程序版本)在动态生成的着色器。由于这些参数的所有可能的组合,导致着色器数量迅速增加。简而言之,Flutter 使用程序(app、Flutter 和 Skia 代码)生成一些其他程序(着色器)可能太大而无法预先计算并与应用程序捆绑。

    Why not just compile or warm up all possible shaders?

    If there were only a limited number of possible shaders, then Flutter could compile all of them when an application is built. However, for the best overall performance, the Skia GPU backend used by Flutter dynamically generates shaders based on many parameters at runtime (for example draws, device models, and driver versions). Due to all possible combinations of those parameters, the number of possible shaders multiplies quickly. In short, Flutter uses programs (app, Flutter, and Skia code) to generate some other programs (shaders). The number of possible shader programs that Flutter can generate is too large to precompute and bundle with an application.

  2. 不同设备下 SkSLs 捕捉的着色器编译能通用吗

    理论上不保证一个设备的 SkSLs 能够帮助另一个设备(但也不会因为 SkSLs 在不同的设备上的不匹配导致其他问题)。在这个 SkSL-based warmup issue 的表格中,SkSLs 几乎都表现得出乎意料的好,即使是 1)在 iOS 设备上捕捉 SkSLs 然后再 Android 设备上使用,或是 2)SkSLs 在模拟器上捕捉,然后用在真机上。由于 Flutter 团队的实验室设备数量有限,我们目前没有足够的数据来支持跨设备有效性。我们希望您向我们提供更多数据,以了解它如何在大范围的生效—— FrameTiming 可用于计算 release 模式下的最差帧的光栅化时间;最坏的帧光栅化时间是衡量着色器编译卡顿严重程度的一个很好的指标。

    Can SkSLs captured from one device help shader compilation jank on another device?

    Theoretically, there’s no guarantee that the SkSLs from one device would help on another device (but they also won’t cause any troubles if SkSLs aren’t compatible across devices). Practically, as shown in the table on this SkSL-based warmup issue, SkSLs work surprisingly well even if 1) SkSLs are captured from iOS and then applied to Android devices, or 2) SkSLs are captured from emulators and then applied to real mobile devices. As the Flutter team has only a limited number of devices in the lab, we currently don’t have enough data to provide a big picture of cross-device effectiveness. We’d love you to provide us more data points to see how it works in the wild—FrameTiming can be used to compute the worst frame rasterization time in release mode; the worst frame rasterization time is a good indicator on how severe the shader compilation jank is.

  3. 为什么不能创建一个「超级着色器」并只编译一次?

    有人建议创建一个超大的着色器实现了 Skia 的所有功能,并在优化的同时使用该着色器定制正在编译的着色器。

    这类似于 海豚模拟器使用的解决方案

    在实践中,我们相信为 Flutter(更具体地来说,是为 Skia)这样做将是不切实际的。这样的着色器会非常大,本质上在 GPU 上重新实现所有 Skia。这本身需要很长时间来编译,从而引入更多的卡顿;即使在编译时,它也不一定足够快以避免卡顿;它还可能会引入保真度问题(例如闪烁)着色器和「超级着色器」之间因为优化后的精确渲染可能存在差异。

    Flutter 和 Skia 是开源的,如果这是你感兴趣的东西的话,我们希望看到沿着这些思路的概念被验证。要开始,请请参阅我们的 贡献指南

    Why can’t you create a single “ubershader” and just compile that once?

    One idea that people sometimes suggest is to create a single large shader that implements all of Skia’s features, and use that shader while the more optimized bespoke shaders are being compiled.

    This is similar to a solution used by the Dolphin Emulator.

    In practice we believe implementing this for Flutter (or more specifically for Skia) would be impractical. Such a shader would be fantastically large, essentially reimplementing all of Skia on the GPU. This would itself take a long time to compile, thus introducing more jank; it would not necessarily be fast enough to avoid jank even when compiled; and it would likely introduce fidelity issues (e.g. flickering) since there would likely be differences in precise rendering between the optimized shaders and the “ubershader”.

    That said, Flutter and Skia are open source and we are eager to see proofs-of-concept along these lines if this is something that interests you. To get started, please see our contribution guidelines.

  4. 如果 flutter 工具可以做 X 就可以简化这些过程!

    围绕着色器预热的工具有多种可能的方式可以改进。有些已经在想法已经被列在我们的 Early-onset jank 这个 GitHub 的项目下。请在你想要的功能下点赞,让我们知道什么对你非常重要。如果没有已经存在的功能需求的话,请提交一个新请求。

    This process would be easier if the flutter tool could do X!

    There are a number of possible ways that the tooling around shader warm-up could be improved. Some are already listed as ideas under our Early-onset jank project on GitHub. Please let us know what is important to you by giving a thumbs-up to your feature request, or by filing a new one if one doesn’t already exist.

将要做的事

Future work

On both Android and iOS, shader warm-up has a few drawbacks:

  1. The size of the deployed app is larger because it contains the bundled shaders.
  2. App startup latency is longer because the bundled shaders need to be precompiled.
  3. Most importantly, we are not happy with the developer experience of shader warm-up. In particular we view the process of performing training runs, and reasoning about the trade-offs imposed by (1) and (2) to be too onerous.

在 Android 和 iOS 上,着色器预热有一些缺点:

  1. 交付的应用包体积增加了,因为它包含了一些绑定的着色器。
  2. 应用启动等待时间变长,因为这些绑定的着色器需要进行编译。
  3. 最重要的是,我们对着色器预热的开发体验很不满意。特别是我们发现由于 (1) 和 (2) 导致执行训练运行和推理的过程的权衡过于繁重。

因此,我们正在 继续研究 解决着色器编译卡顿的方法,并且不依赖于着色器预热,更常见的卡顿。特别是,我们与 Skia 共同 减少着色器的数量,它根据 Flutter 的请求返回 Flutter 可以实现多少可以与 Flutter 引擎捆绑的 一小组静态定义的着色器。敬请期待更多进展!

Therefore, we are continuing to investigate approaches to shader compilation jank, and jank more generally, that do not rely on shader warm-up. In particular, we are both working with Skia to reduce the number of shaders it generates in response to Flutter’s requests, as well as investigating how much of Flutter could be implemented with a small set of statically defined shaders that we could bundle with the Flutter Engine. Stay tuned for more progress!

如果你对 SkSL 着色器预热有任何疑问,请在 Issue 60313 以及 Issue 53607 提问。如果你有一般的着色器预热的问题,请链接到 Issue 32170

If you have questions on SkSL shader warm-up, please comment on Issue 60313 and Issue 53607. If you have general shader warm-up questions, please refer to Issue 32170.

性能指标

如果你想获取完整的 Flutter 性能指标列表,访问以下的站点,点击 Query ,然后选择 testsub_result

For a complete list of performance metrics Flutter measures per commit, visit the following sites, click Query, and filter the test and sub_result fields:

Performance FAQ

This page collects some frequently asked questions about evaluating and debugging Flutter’s performance.

More thoughts about performance

目录

What is performance?

Performance is a set of quantifiable properties of a performer.

In this context, performance isn’t the execution of an action itself; it’s how well something or someone performs. Therefore, we use the adjective performant.

While the how well part can, in general, be described in natural languages, in our limited scope, the focus is on something that is quantifiable as a real number. Real numbers include integers and 0/1 binaries as special cases. Natural language descriptions are still very important. For example, a news article that heavily criticizes Flutter’s performance by just using words without any numbers (a quantifiable value) could still be meaningful, and it could have great impacts. The limited scope is chosen only because of our limited resources.

The required quantity to describe performance is often referred to as a metric.

To navigate through countless performance issues and metrics, you can categorize based on performers.

For example, most of the content on this website is about the Flutter app performance, where the performer is a Flutter app. Infra performance is also important to Flutter, where the performers are build bots and CI task runners: they heavily affect how fast Flutter can incorporate code changes, to improve the app’s performance.

Here, the scope was intentionally broadened to include performance issues other than just app performance issues because they can share many tools regardless of who the performers are. For example, Flutter app performance and infra performance might share the same dashboard and similar alert mechanisms.

Broadening the scope also allows performers to be included that traditionally are easy to ignore. Document performance is such an example. The performer could be an API doc of the SDK, and a metric could be: the percentage of readers who find the API doc useful.

Why is performance important?

Answering this question is not only crucial for validating the work in performance, but also for guiding the performance work in order to be more useful. The answer to “why is performance important?” often is also the answer to “how is performance useful?”

Simply speaking, performance is important and useful because, in the scope, performance must have quantifiable properties or metrics. This implies:

  1. A performance report is easy to consume.
  2. Performance has little ambiguity.
  3. Performance is comparable and convertible.
  4. Performance is fair.

Not that non-performance, or non-measurable issues or descriptions are not important. They’re meant to highlight the scenarios where performance can be more useful.

1. A performance report is easy to consume

Performance metrics are numbers. Reading a number is much easier than reading a passage. For example, it probably takes an engineer 1 second to consume the performance rating as a number from 1 to 5. It probably takes the same engineer at least 1 minute to read the full, 500-word feedback summary.

If there are many numbers, it’s easy to summarize or visualize them for quick consumption. For example, you can quickly consume millions of numbers by looking at its histogram, average, quantiles, and so on. If a metric has a history of thousands of data points, then you can easily plot a timeline to read its trend.

On the other hand, having n number of 500-word texts almost guarantees an n-time cost to consume those texts. It would be a daunting task to analyze thousands of historical descriptions, each having 500 words.

2. Performance has little ambiguity

Another advantage of having performance as a set of numbers is its unambiguity. When you want an animation to have a performance of 20 ms per frame or 50 fps, there’s little room for different interpretations about the numbers. On the other hand, to describe the same animation in words, someone might call it good, while someone else might complain that it’s bad. Similarly, the same word or phrase could be interpreted differently by different people. You might interpret an OK frame rate to be 60 fps, while someone else might interpret it to be 30 fps.

Numbers can still be noisy. For example, the measured time per frame might be a true computation time of this frame, plus a random amount of time (noise) that CPU/GPU spends on some unrelated work. Hence, the metric will fluctuate. Nevertheless, there’s no ambiguity of what the number means. And, there are also rigorous theory and testing tools to handle such noise. For example, you could take multiple measurements to estimate the distribution of a random variable, or you could take the average of many measurements to eliminate the noise by the law of large numbers.

3. Performance is comparable and convertible

Performance numbers not only have unambiguous meanings, but they also have unambiguous comparisons. For example, there’s no doubt that 5 is greater than 4. On the other hand, it might be subjective to figure out whether excellent is better or worse than superb. Similarly, could you figure out whether epic is better than legendary? Actually, the phrase strongly exceeds expectations could be better than superb in someone’s interpretation. It only becomes unambiguous and comparable after a definition that maps strongly exceeds expectations to 4 and superb to 5.

Numbers are also easily convertible using formulas and functions. For example, 60 fps can be converted to 16.67 ms per frame. A frame’s rendering time x (ms) can be converted to a binary indicator isSmooth = [x <= 16] = (x <= 16 ? 1 :0). Such conversion can be compounded or chained, so you can get a large variety of quantities using a single measurement without any added noise or ambiguity. The converted quantity can then be used for further comparisons and consumption. Such conversions are almost impossible if you’re dealing with natural languages.

4. Performance is fair

If issues rely on verbose words to be discovered, then an unfair advantage is given to people who are more verbose (more willing to chat or write) or those who are closer to the development team, who have a larger bandwidth and lower cost for chatting or face-to-face meetings.

By having the same metrics to detect problems no matter how far away or how silent the users are, we can treat all issues fairly. That, in turn, allows us to focus on the right issues that have greater impact.

How to make performance useful

The following summarizes the 4 points discussed here, from a slightly different perspective:

  1. Make performance metrics easy to consume. Do not overwhelm the readers with a lot of numbers (or words). If there are many numbers, then try to summarize them into a smaller set of numbers (for example, summarize many numbers into a single average number). Only notify readers when the numbers change significantly (for example, automatic alerts on spikes or regressions).

  2. Make performance metrics as unambiguous as possible. Define the unit that the number is using. Precisely describe how the number is measured. Make the number easily reproducible. When there’s a lot of noise, try to show the full distribution, or eliminate the noise as much as possible by aggregating many noisy measurements.

  3. Make it easy to compare performance. For example, provide a timeline to compare the current version with the old version. Provide ways and tools to convert one metric to another. For example, if we can convert both memory increase and fps drops into the number of users dropped or revenue lost in dollars, then we can compare them and make an informed trade-off.

  4. Make performance metrics monitor a population that is as wide as possible, so no one is left behind.

混淆 Dart 代码

目录

Code obfuscation is the process of modifying an app’s binary to make it harder for humans to understand. Obfuscation hides function and class names in your compiled Dart code, making it difficult for an attacker to reverse engineer your proprietary app.

The following list describes which platforms support the obfuscation process described in this page:

Android/iOS
Supported as of Flutter 1.16.2. To obfuscate an app built against an earlier version of Flutter, use the obfuscation instructions on the Flutter wiki.
macOS
macOS (in alpha as of Flutter 1.13), supports obfuscation as of Flutter 1.16.2.
Linux/Windows
Not yet supported.
web
Obfuscation is not supported for web apps, but a web app can be minified, which is similar. When you build a release version of a Flutter web app, it is automatically minified. For more information, see Build and release a web app.

Flutter’s code obfuscation, when supported, works only on a release build.

Obfuscating your app

To obfuscate your app, build a release version using the --obfuscate flag, combined with the --split-debug-info flag. The --split-debug-info flag specifies the directory where Flutter can output debug files. This command generates a symbol map. The apk, appbundle, ipa, ios, and ios-framework targets are currently supported. (macos and aar are supported on the master and dev channels.) For example:

flutter build apk --obfuscate --split-debug-info=/<project-name>/<directory>

Once you’ve obfuscated your binary, save the symbols file. You need this if you later want to de-obfuscate a stack trace.

Note that the --split-debug-info flag can also be used by itself. In fact, it can dramatically reduce code size. For more information on app size, see Measuring your app’s size.

For detailed information on these flags, run the help command for your specific target, for example:

flutter build apk -h

If these flags are not listed in the output, run flutter --version to check your version of Flutter.

Reading an obfuscated stack trace

To debug a stack trace created by an obfuscated app, use the following steps to make it human readable:

  1. Find the matching symbols file. For example, a crash from an Android arm64 device would need app.android-arm64.symbols.

  2. Provide both the stack trace (stored in a file) and the symbols file to the flutter symbolize command. For example:

flutter symbolize -i <stack trace file> -d /out/android/app.android-arm64.symbols

For more information on the symbolize command, run flutter symbolize -h.

Caveat

Be aware of the following when coding an app that will eventually be an obfuscated binary.

expect(foo.runtimeType.toString(), equals('Foo'))

配置 flavors 构建双端

如何使用 flavors 配置多渠道构建?社区已经提供许多有用的文章。这些文章直接讲述如何在 iOS 和 Android 上配置 flavor。

Do you need to set up product flavors for different development environments or release types? The community has written some articles and packages you might find useful. These articles address flavors for both iOS and Android.

请尝试下面列出的 package 来配置 flavor:

The following packages are listed alphabetically:

构建和发布为 Android 应用

目录

在一般的开发过程中,我们可以使用 flutter run 命令,或者 IntelliJ 工具栏中的 RunDebug 来测试 app。这时候,Flutter 默认会为我们构建 app 的调试版本。

During a typical development cycle, you test an app using flutter run at the command line, or by using the Run and Debug options in your IDE. By default, Flutter builds a debug version of your app.

当想要发布 app 时,比如 发布到 Google Play Store,可以按照以下步骤来准备 Android 平台的 发布 版本。本页面的内容包含如下主题:

When you’re ready to prepare a release version of your app, for example to publish to the Google Play Store, this page can help. Before publishing, you might want to put some finishing touches on your app. This page covers the following topics:

添加启动图标

Adding a launcher icon

当我们创建一个新的 Flutter app 的时候,它会有一个默认的启动图标。要自定义这个图标,可以参考使用 flutter_launcher_icons 这个 package。

When a new Flutter app is created, it has a default launcher icon. To customize this icon, you might want to check out the flutter_launcher_icons package.

或者,如果我们想手动操作,可以参考以下方法:

Alternatively, you can do it manually using the following steps:

  1. 查看 Material Design Product Icons 指南中图标设计部分。

    Review the Material Design product icons guidelines for icon design.

  2. <app dir>/android/app/src/main/res/ 目录下,把我们的图标文件放在以 配置限定符 命名的文件夹中。类似默认的 mipmap- 文件夹这样的命名方式。

    In the [project]/android/app/src/main/res/ directory, place your icon files in folders named using configuration qualifiers. The default mipmap- folders demonstrate the correct naming convention.

  3. AndroidManifest.xml 中,更新 application 标签中的 android:icon 属性来引用上一步骤中我们自己的图标文件 (例如,<application android:icon="@mipmap/ic_launcher" ...)。

    In AndroidManifest.xml, update the application tag’s android:icon attribute to reference icons from the previous step (for example, <application android:icon="@mipmap/ic_launcher" ...).

  4. flutter run 运行 app,检查启动程序中的 app 图标是否已经替换成我们自己的图标文件。

    To verify that the icon has been replaced, run your app and inspect the app icon in the Launcher.

启用 Material 组件

Enabling Material Components

如果你的应用使用了 平台视图 (Platform Views),你可能要通过 Android 平台的入门指南文档 中的步骤使用 Material 组件:

If your app uses Platform Views, you may want to enable Material Components by following the steps described in the Getting Started guide for Android.

举个例子:

For example:

  1. <my-app>/android/app/build.gradle 文件中添加 Android Material 组件依赖:

    Add the dependency on Android’s Material in <my-app>/android/app/build.gradle:

dependencies {
    // ...
    implementation 'com.google.android.material:material:<version>'
    // ...
}

查看最新的版本,请访问 Google Maven 仓库

To find out the latest version, visit Google Maven.

  1. <my-app>/android/app/src/main/res/values/styles.xml 文件中设置亮色主题:

    Set the light theme in <my-app>/android/app/src/main/res/values/styles.xml:

-<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+<style name="NormalTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
  1. Set the dark theme in <my-app>/android/app/src/main/res/values-night/styles.xml
-<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+<style name="NormalTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">

为 app 签名

Signing the app

要想把 app 发布到 Play store,还需要给 app 一个数字签名。我们可以采用以下步骤来为 app 签名:

To publish on the Play Store, you need to give your app a digital signature. Use the following instructions to sign your app.

Android 中有两种签名密钥: 部署和上传。终端用户下载到的 .apk 文件是被部署密钥签名过的文件,上传密钥用于验证开发者上载到 Play 商店的 .aab 或 .apk 文件。上传密钥是给予部署密钥重新签名的密钥,上载 Play 商店时候需要用到。

On Android, there are two signing keys: deployment and upload. The end-users download the .apk signed with the ‘deployment key’. An ‘upload key’ is used to authenticate the .aab / .apk uploaded by developers onto the Play Store and is re-signed with the deployment key once in the Play Store.

创建一个用于上传的密钥库

Create an upload keystore

如果你已经有一个密钥库了,可以直接跳到下一步,如果还没有,需要参考下面的方式创建一个:

If you have an existing keystore, skip to the next step. If not, create one by either:

从 app 中引用密钥库

Reference the keystore from the app

创建一个名为 [project]/android/key.properties 的文件,它包含了密钥库位置的定义:

Create a file named [project]/android/key.properties that contains a reference to your keystore:

storePassword=<上一步骤中的密码>
keyPassword=<上一步骤中的密码>
keyAlias=upload
storeFile=<密钥库的位置,e.g. /Users/<用户名>/upload-keystore.jks>

在 gradle 中配置签名

Configure signing in gradle

在以 release 模式下构建你的应用时,修改 [project]/android/app/build.gradle 文件,以通过 gradle 配置你的上传密钥。

Configure gradle to use your upload key when building your app in release mode by editing the [project]/android/app/build.gradle file.

  1. android 代码块之前将你 properties 文件的密钥库信息添加进去:

    Add the keystore information from your properties file before the android block:

       def keystoreProperties = new Properties()
       def keystorePropertiesFile = rootProject.file('key.properties')
       if (keystorePropertiesFile.exists()) {
           keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
       }
    
       android {
             ...
       }
    

    key.properties 文件加载到 keystoreProperties 对象中。

    Load the key.properties file into the keystoreProperties object.

  2. 找到 buildTypes 代码块:

    Find the buildTypes block:

       buildTypes {
           release {
               // TODO: Add your own signing config for the release build.
               // Signing with the debug keys for now,
               // so `flutter run --release` works.
               signingConfig signingConfigs.debug
           }
       }
    

    将其替换为我们的配置内容:

    And replace it with the following signing configuration info:

       signingConfigs {
           release {
               keyAlias keystoreProperties['keyAlias']
               keyPassword keystoreProperties['keyPassword']
               storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
               storePassword keystoreProperties['storePassword']
           }
       }
       buildTypes {
           release {
               signingConfig signingConfigs.release
           }
       }
    

现在我们 app 的发布版本就会被自动签名了。

Release builds of your app will now be signed automatically.

有关应用签名的更多信息,请查看 developer.android.com 的 为您的应用设置签名

For more information on signing your app, see Sign your app on developer.android.com.

使用 R8 压缩你的代码

Shrinking your code with R8

R8 是谷歌推出的最新代码压缩器,当你打包 release 版本的 APK 或者 AAB 时会默认开启。要关闭 R8,请运行 flutter build apkflutter build appbundle 时加上 --no-shrink 参数。

R8 is the new code shrinker from Google, and it’s enabled by default when you build a release APK or AAB. To disable R8, pass the --no-shrink flag to flutter build apk or flutter build appbundle.

Enabling multidex support

When writing large apps or making use of large plugins, you may encounter Android’s dex limit of 64k methods when targeting a minimum API of 20 or below. This may also be encountered when running debug versions of your app via flutter run that does not have shrinking enabled.

Flutter tool supports easily enabling multidex. The simplest way is to opt into multidex support when prompted. The tool detects multidex build errors and will ask before making changes to your Android project. Opting in allows Flutter to automatically depend on androidx.multidex:multidex and use a generated FlutterMultiDexApplication as the project’s application.

You might also choose to manually support multidex by following Android’s guides and modifying your project’s Android directory configuration. A multidex keep file must be specified to include:

io/flutter/embedding/engine/loader/FlutterLoader.class
io/flutter/util/PathUtils.class

Also, include any other classes used in app startup. See the official Android documentation for more detailed guidance on adding multidex support manually.

检查 app manifest 文件

Reviewing the app manifest

检查位于 <app dir>/android/app/src/main 的默认 App Manifest 文件 AndroidManifest.xml,并确认各个值都设置正确,特别是:

Review the default App Manifest file, AndroidManifest.xml, located in [project]/android/app/src/main and verify that the values are correct, especially the following:

application
编辑 application 标签中的 android:label 来设置 app 的最终名字。

application
Edit the android:label in the application tag to reflect the final name of the app.

uses-permission
如果你的代码需要互联网交互,请加入 android.permission.INTERNET 权限标签。标准开发模版里并未加入这个权限(但是 Flutter debug 模版加入了这个权限),加入这个权限是为了允许 Flutter 工具和正在运行的 app 之间的通信。

uses-permission
Add the android.permission.INTERNET permission if your application code needs Internet access. The standard template does not include this tag but allows Internet access during development to enable communication between Flutter tools and a running app.

检查构建配置

Reviewing the build configuration

检查位于 <app dir>/android/app 的默认 Gradle 构建文件,并确认各个值都设置正确,特别是下面 defaultConfig 块中的值:

Review the default Gradle build file, build.gradle, located in [project]/android/app and verify the values are correct, especially the following values in the defaultConfig block:

applicationId
指定最终的,唯一的(Application Id)appid

applicationId
Specify the final, unique (Application Id)appid

versionCode & versionName
指定 app 的内部版本号,以及用于显示的版本号,这可以通过设置 pubspec.yaml 文件中 version 属性来做。具体可以参考 版本文档 中的版本信息指南。

versionCode & versionName
Specify the internal app version number, and the version number display string. You can do this by setting the version property in the pubspec.yaml file. Consult the version information guidance in the versions documentation.

minSdkVersioncompilesdkVersiontargetSdkVersion
指定应用运行所需要的最低 API 级别 minSdkVersion、编译 API 级别 compilesdkVersion 以及目标 API 级别 targetSdkVersion。具体可以参考 Android 开发者网站上的 版本文档 中的 API 版本的部分。

minSdkVersion, compilesdkVersion, & targetSdkVersion
Specify the minimum API level, the API level on which the app was compiled, and the maximum API level on which the app is designed to run. Consult the API level section in the versions documentation for details.

buildToolsVersion
指定应用所需的 Android SDK 构建工具的版本,或者你可以在 Android Studio 里使用 Android Gradle 插件 (AGP),它可以自动设置导入你应用所需的构建工具版本,这样就无需过多操心这个属性啦。

buildToolsVersion
Specify the version of Android SDK Build Tools that your app uses. Alternatively, you can use the Android Gradle Plugin in Android Studio, which will automatically import the minimum required Build Tools for your app without the need for this property.

为发布构建应用程序

Building the app for release

当要发布到 Play Store 时,你有两种发布方式的选择:

You have two possible release formats when publishing to the Play Store.

构建一个 app bundle

Build an app bundle

这个部分描述了如何构建一个发布的 app bundle。如果在前面的部分已经完成了签名步骤,发布的 bundle 会被签名。这时你也许想要 混淆你的 Dart 代码 以加大反编译难度。混淆你的代码需要在 build 的时候添加一些标志,并维护其他文件以消除反编译的堆栈跟踪。

This section describes how to build a release app bundle. If you completed the signing steps, the app bundle will be signed. At this point, you might consider obfuscating your Dart code to make it more difficult to reverse engineer. Obfuscating your code involves adding a couple flags to your build command, and maintaining additional files to de-obfuscate stack traces.

使用如下命令:

From the command line:

  1. 运行 cd [project]

    Enter cd [project]

  2. 运行 flutter build appbundle。 (运行 flutter build 默认构建一个发布版本。)

    Run flutter build appbundle
    (Running flutter build defaults to a release build.)

你的应用的 release bundle 会被创建到 <app dir>/build/app/outputs/bundle/release/app.aab.

The release bundle for your app is created at [project]/build/app/outputs/bundle/release/app.aab.

此 app bundle 会默认地包含为 armeabi-v7a (ARM 32-bit)、arm64-v8a (ARM 64-bit) 以及 x86-64 (x86 64-bit) 编译的 Dart 和 Fluter 运行时代码。

By default, the app bundle contains your Dart code and the Flutter runtime compiled for armeabi-v7a (ARM 32-bit), arm64-v8a (ARM 64-bit), and x86-64 (x86 64-bit).

测试 app bundle

Test the app bundle

一个 app bundle 可以用多种方法测试,这里介绍两种。

An app bundle can be tested in multiple ways—this section describes two.

离线使用 bundle tool

Offline using the bundle tool

  1. 如果你还没准备好,可以从 GitHub 仓库 下载 bundletool

    If you haven’t done so already, download bundletool from the GitHub repository.

  2. 从你的 app bundle 生成 APKs

    Generate a set of APKs from your app bundle.

  3. 将这 APKs 部署到 已连接的设备。

    Deploy the APKs to connected devices.

在线使用 Google Play

Online using Google Play

  1. 上传你的 bundle 到 Google Play 去测试它。或者在正式发布之前用 alpha 或 beta 频道去测试。

    Upload your bundle to Google Play to test it. You can use the internal test track, or the alpha or beta channels to test the bundle before releasing it in production.

  2. 按照 这些步骤把你的 bundle 上传到 Play Store。

    Follow these steps to upload your bundle to the Play Store.

构建一个 APK

Build an APK

虽然 app bundle 比 APKs 更被推荐使用,但是有一些 Store 目前还不支持 app bundle方式。这种情况下,要为各种目标 ABI (Application Binary Interface) 分别构建发布的 APK 文件。

Although app bundles are preferred over APKs, there are stores that don’t yet support app bundles. In this case, build a release APK for each target ABI (Application Binary Interface).

如果你完成签名步骤,APK 就被签名了。这时你也许想要 混淆你的 Dart 代码 以加大反编译难度。混淆你的代码需要在构建时添加一些参数。

If you completed the signing steps, the APK will be signed. At this point, you might consider obfuscating your Dart code to make it more difficult to reverse engineer. Obfuscating your code involves adding a couple flags to your build command.

使用如下命令:

From the command line:

  1. 输入命令 cd [project]

    Enter cd [project]

  2. 运行 flutter build apk --split-per-abi
    flutter build 默认带有 --release 参数。)

    Run flutter build apk --split-per-abi
    (The flutter build command defaults to --release.)

这个命令会生成如下三个 APK 文件

This command results in three APK files:

如果移除 --split-per-abi 将会生成一个包含 所有 目标 ABI 的 fat APK 文件。这种 APK 文件将会在比单独构建的 APK 文件尺寸要大,会导致用户下载一些不适用于其设备架构的二进制文件。

Removing the --split-per-abi flag results in a fat APK that contains your code compiled for all the target ABIs. Such APKs are larger in size than their split counterparts, causing the user to download native binaries that are not applicable to their device’s architecture.

在设备上安装 APK 文件

Install an APK on a device

按照如下这些步骤,将前一步中构建出来的 APK 安装到 Android 设备上。

Follow these steps to install the APK on a connected Android device.

使用如下命令:

From the command line:

  1. 用 USB 线将 Android 设备连接到电脑上;

    Connect your Android device to your computer with a USB cable.

  2. 输入命令 cd [project]

    Enter cd [project].

  3. 运行 flutter install

    Run flutter install.

发布到 Google Play Store

Publishing to the Google Play Store

要了解如何发布一个 app 到 Google Play Store,可以参考 Google Play 发布文档

For detailed instructions on publishing your app to the Google Play Store, see the Google Play launch documentation.

更新应用版本号

Updating the app’s version number

每个应用默认的初始版本号是 1.0.0。若要更新它,请转到 pubspec.yaml 文件并更新以下内容:

The default version number of the app is 1.0.0. To update it, navigate to the pubspec.yaml file and update the following line:

version: 1.0.0+1

版本号由三个点分隔的数字组成,例如上面样例中的 1.0.0。然后是可选的构建号,例如上面样例中的 1,以 + 分隔。

The version number is three numbers separated by dots, such as 1.0.0 in the example above, followed by an optional build number such as 1 in the example above, separated by a +.

版本号与构建号都可以在 Flutter 打包时分别使用 --build-name--build-number 重新指定。

Both the version and the build number may be overridden in Flutter’s build by specifying --build-name and --build-number, respectively.

在 Android 中,build-number 被用作 versionCodebuild-name 将作为 versionName 使用。更多信息请参考 Android 文档中的 为你的应用添加版本

In Android, build-name is used as versionName while build-number used as versionCode. For more information, see Version your app in the Android documentation.

在更新完 pubspec 文件中的版本号之后,在项目根目录下运行 flutter pub get,或者使用 IDE 中的 Pub get 按钮。这将会更新 local.properties 文件中的 versionNameversionCode,之后它会在你构建 Flutter 应用的时候更新 build.gradle

After updating the version number in the pubspec file, run flutter pub get from the top of the project, or use the Pub get button in your IDE. This updates the versionName and versionCode in the local.properties file, which are later updated in the build.gradle file when you rebuild the Flutter app.

Android发布常见问题

Android release FAQ

这里是一些关于安卓应用程序发布的常见问题。

Here are some commonly asked questions about deployment for Android apps.

我应该什么时候构建 app bundles 而不是 APKs?

When should I build app bundles versus APKs?

Google Play Store 相对于 APKs 更建议你发布 app bundles,因为那样应用程序会更有效率地交付给你的用户。但是,如果你想将应用程序发布到其他的应用商店,APK可能是唯一选项。

The Google Play Store recommends that you deploy app bundles over APKs because they allow a more efficient delivery of the application to your users. However, if you’re distributing your application by means other than the Play Store, an APK may be your only option.

什么是 fat APK?

What is a fat APK?

一个 fat APK 是一个包含了支持多个 ABI 架构的 APK 文件。这样做的好处是单个 APK 可以运行在多个架构上,因此具有更广泛的兼容性。但同时缺点就是文件体积会比较大,导致用户在安装你的应用程序时会下载和储存更多的字节。当构建 APK 而不是 app bundles 时强烈建议分开构建 APK,如 build an APK 所描述的那样,使用 --split-per-abi 指令。

A fat APK is a single APK that contains binaries for multiple ABIs embedded within it. This has the benefit that the single APK runs on multiple architectures and thus has wider compatibility, but it has the drawback that its file size is much larger, causing users to download and store more bytes when installing your application. When building APKs instead of app bundles, it is strongly recommended to build split APKs, as described in build an APK using the --split-per-abi .

哪些目标架构是被支持的?

What are the supported target architectures?

当使用 release 模式构建你的应用程序时, Flutter app 可以基于 armeabi-v7a (ARM 32 位)、 arm64-v8a (ARM 64 位) 以及 x86-64 (x86 64 位) 被编译。 Flutter 目前不支持 x86 Android (参考 Issue 9253).

When building your application in release mode, Flutter apps can be compiled for armeabi-v7a (ARM 32-bit), arm64-v8a (ARM 64-bit), and x86-64 (x86 64-bit). Flutter does not currently support building for x86 Android (See Issue 9253).

如何为一个使用 flutter build appbundle 创建的 app bundle 签名?

How do I sign the app bundle created by flutter build appbundle?

See Signing the app.

如何使用 Android Studio 构建一个发布?

How do I build a release from within Android Studio?

在Android Studio中, 打开你的 app 文件夹下的 android/ 文件夹. 然后在项目面板中选择 build.gradle (Module: app) :

In Android Studio, open the existing android/ folder under your app’s folder. Then, select build.gradle (Module: app) in the project panel:

screenshot of gradle build script menu

接下来,选择构建变体。在主菜单中点击 Build > Select Build Variant。从 Build Variants 面板中选择任意一个变体(默认是 debug)。

Next, select the build variant. Click Build > Select Build Variant in the main menu. Select any of the variants in the Build Variants panel (debug is the default):

screenshot of build variant menu

生成的 app bundle 或 APK 文件会在你的 app 所在文件夹下的 build/app/outputs 文件夹下。

The resulting app bundle or APK files are located in build/app/outputs within your app’s folder.

构建和发布为 iOS 应用

目录

这个教程将为你提供关于如何将 Flutter App 发布到 App StoreTestFlight 的说明。

This guide provides a step-by-step walkthrough of releasing a Flutter app to the App Store and TestFlight.

预先准备

Preliminaries

构建和发布一个 macOS 应用需要使用 Xcode,你必须要有一个运行着 macOS 系统的设备来学习本指南文档。

Xcode is required to build and release your app. You must use a device running macOS to follow this guide.

在开始发布你的 app 的进程之前,确保你已经看过了 Apple 的 App Store 审核指南

Before beginning the process of releasing your app, ensure that it meets Apple’s App Review Guidelines.

想要发布你的 app 到 App Store,你需要注册 Apple Developer Program。你可以在苹果的 选择会员资格(开发者类型) 中查看到关于多种不同会员类型的选择。

In order to publish your app to the App Store, you must first enroll in the Apple Developer Program. You can read more about the various membership options in Apple’s Choosing a Membership guide.

在 App Store Connect 上注册你的 App

Register your app on App Store Connect

App Store Connect (曾经的 iTunes Connet)是你将会管理应用生命周期的地方。你将会定义应用的名称和描述以及截图,设置价格,并管理发布到 App Store 和 Testflight。

Manage your app’s life cycle on App Store Connect (formerly iTunes Connect). You define your app name and description, add screenshots, set pricing, and manage releases to the App Store and TestFlight.

注册你的 app 需要两步:登记唯一的套装 ID(Bundle ID),并在你的 App Store Connect 中创建一个 app。

Registering your app involves two steps: registering a unique Bundle ID, and creating an application record on App Store Connect.

关于更多 App Store Connect 的细节,查看 App Store Connect 指南。

For a detailed overview of App Store Connect, see the App Store Connect guide.

登记套装 ID

Register a Bundle ID

每一个 iOS 应用都与一个在 Apple 登记的唯一的套装 ID 关联。要为你的应用登记一个套装 ID,请参考下面的步骤:

Every iOS application is associated with a Bundle ID, a unique identifier registered with Apple. To register a Bundle ID for your app, follow these steps:

  1. 在你的开发者账号页面打开 App IDs 页面。

    Open the App IDs page of your developer account.

  2. 点击 + 来创建一个新的套装 ID。

    Click + to create a new Bundle ID.

  3. 输入一个 App 名称,选择 Explicit App ID,然后输入一个 ID。

    Enter an app name, select Explicit App ID, and enter an ID.

  4. 选择你的 App 将要使用的服务,然后点击 继续

    Select the services your app uses, then click Continue.

  5. 在下一页,确认细节并点击 注册 来注册你的 Bundle ID。

    On the next page, confirm the details and click Register to register your Bundle ID.

在 App Store Connect 创建一个应用记录

Create an application record on App Store Connect

在 App Store Connect 中注册你的应用:

Register your app on App Store Connect:

接下来,你需要在 App Store Connect 注册你的应用:

Next, you’ll register your app on App Store Connect:

  1. 在你的浏览器里打开 App Store Connect

    Open App Store Connect in your browser.

  2. 在 App Store Connect 的落地页,点击 My Apps

    On the App Store Connect landing page, click My Apps.

  3. 在我的 app 页面的顶部左侧,点击 + ,然后选择 New App

    Click + in the top-left corner of the My Apps page, then select New App.

  4. 在出现的表单中填写你的 app 细节。在平台部分,确保 iOS 被选中。由于 Flutter 暂时不支持 tvOS,保持该选项为未选。点击 Create

    Fill in your app details in the form that appears. In the Platforms section, ensure that iOS is checked. Since Flutter does not currently support tvOS, leave that checkbox unchecked. Click Create.

  5. 跳转到你的应用详情,然后从侧边栏选择 App Information

    Navigate to the application details for your app and select App Information from the sidebar.

  6. 在基础信息部分,选择你在前一步注册的套装 ID。

    In the General Information section, select the Bundle ID you registered in the preceding step.

想要获取更多信息,可以看这个帮助页面 添加 App 至您的帐户

For a detailed overview, see Add an app to your account.

检查 Xcode 项目设置

Review Xcode project settings

在这一步,你需要在 Xcode 工作空间检查绝大多数重要设置。关于更多的步骤和描述,查看 为 App 分发做准备

This step covers reviewing the most important settings in the Xcode workspace. For detailed procedures and descriptions, see Prepare for app distribution.

在 Xcode 中跳转到你的目标设置:

Navigate to your target’s settings in Xcode:

  1. 在 Xcode 中,打开你的 App 的 ios 目录中的 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace in your app’s ios folder.

  2. 想要看你的 app 设置,在 Xcode 的项目导航栏中选择 Runner

    To view your app’s settings, select the Runner project in the Xcode project navigator. Then, in the main view sidebar, select the Runner target.

  3. 选择 General tab

    Select the General tab.

接下来,你需要验证最重要的配置:

Verify the most important settings.

Identity 部分:

In the Identity section:

Display Name
The display name of your app.

Display Name
应用的名字。

Display Name
The display name of your app.

Bundle Identifier
在 App Store Connect 注册的 App ID。

Bundle Identifier
The App ID you registered on App Store Connect.

Signing & Capabilities 部分:

In the Signing & Capabilities section:

Automatically manage signing
是否需要 Xcode 自动管理 app 签名和设置。这个默认被设置为 true ,对于绝大多数 App 来说都是适用的。对于更复杂的场景,查看 代码签名指南

Automatically manage signing
Whether Xcode should automatically manage app signing and provisioning. This is set true by default, which should be sufficient for most apps. For more complex scenarios, see the Code Signing Guide.

Team
选择关联到你注册的 Apple 开发者账户的团队。如果需要,选择 Add Account…, 然后更新选项。

Team
Select the team associated with your registered Apple Developer account. If required, select Add Account…, then update this setting.

Build Settings 部分:

In the Build Settings section:

iOS Deployment Target
设定你的应用可以支持到的最低的 iOS 版本。 Flutter 支持 iOS 9.0 及其之后的版本,如果你的应用包含了 iOS 9 不支持的 Objective-C 或 Swift 代码,请将这里一并设置为相应所需的最高版本。

iOS Deployment Target
The minimum iOS version that your app supports. Flutter supports iOS 9.0 and later. If your app or plugins include Objective-C or Swift code that makes use of APIs newer than iOS 9, update this setting to the highest required version.

你项目的 General tab 应该看起来像是这样的:

The General tab of your project settings should resemble the following:

Xcode Project Settings

更多关于 App 签名新的介绍,查看文档 创建, 导出, 和删除签名证书

For a detailed overview of app signing, see Create, export, and delete signing certificates.

更新应用的开发版本

Updating the app’s deployment version

如果你在 Xcode 工程里更改了 Deployment Target,你需要打开 Flutter app 的 ios/Flutter/AppframeworkInfo.plist 文件并修改 MinimumOSVersion 值与之匹配。

If you changed Deployment Target in your Xcode project, open ios/Flutter/AppframeworkInfo.plist in your Flutter app and update the MinimumOSVersion value to match.

更新应用版本号

Updating the app’s version number

每个应用默认的初始版本号是 1.0.0。若要更新它,请转到 pubspec.yaml 文件并更新以下内容:

The default version number of the app is 1.0.0. To update it, navigate to the pubspec.yaml file and update the following line:

version: 1.0.0+1

版本号由三个点分隔的数字组成,例如上面样例中的 1.0.0。然后是可选的构建号,例如上面样例中的 1,以 + 分隔。

The version number is three numbers separated by dots, such as 1.0.0 in the example above, followed by an optional build number such as 1 in the example above, separated by a +.

版本号与构建号都可以在 Flutter 打包时分别使用 --build-name--build-number 重新指定。

Both the version and the build number may be overridden in Flutter’s build by specifying --build-name and --build-number, respectively.

在 iOS 中,当 build-number 用作 CFBundleVersion 的时候, build-name 用作 CFBundleShortVersionString。阅读关于 iOS 版本控制的更多信息请参考 Apple 开发者网站提供的 Core Foundation Keys

In iOS, build-name uses CFBundleShortVersionString while build-number uses CFBundleVersion. Read more about iOS versioning at Core Foundation Keys on the Apple Developer’s site.

添加应用图标

Add an app icon

当你创建一个新的 Flutter 应用时,则会创建一个默认的图标。在这一步,你将使用你自己的图标替换占位图标:

When a new Flutter app is created, a placeholder icon set is created. This step covers replacing these placeholder icons with your app’s icons:

  1. 回顾 iOS 的 App Icon 指南。

    Review the iOS App Icon guidelines.

  2. 在 Xcode 项目导航栏,选择 Runner 目录中的 Assets.xcassets,更新占位图标为你自己的 app 的图标。

    In the Xcode project navigator, select Assets.xcassets in the Runner folder. Update the placeholder icons with your own app icons.

  3. 通过执行 flutter run 来验证你的图标是否已经被替换。

    Verify the icon has been replaced by running your app using flutter run.

创建一个构建归档 (build archive)

Create a build archive with Xcode

在这一步,你将创建一个构建归档,并上传到 App Store Connect。

This step covers creating a build archive and uploading your build to App Store Connect.

在开发过程中,你将会使用 debug 模式来完成构建、调试并测试。当你准备好通过 App Store 或 TestFlight 交付你的 app 给用户时,你需要准备一个 release 构建。这时你也许想要 混淆你的 Dart 代码 以加大反编译难度。混淆你的代码需要在 build 的时候添加一些标志,并维护其他文件以消除反编译的堆栈跟踪。

During development, you’ve been building, debugging, and testing with debug builds. When you’re ready to ship your app to users on the App Store or TestFlight, you need to prepare a release build. At this point, you might consider obfuscating your Dart code to make it more difficult to reverse engineer. Obfuscating your code involves adding a couple flags to your build command.

在 Xcode中,配置 app 的版本,并开始构建:

In Xcode, configure the app version and build:

  1. 在 Xcode 中,打开你应用 ios 目录中的 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace in your app’s ios folder.

  2. 在 Xcode 项目导航栏中选择 Runner,然后在设置界面侧边栏选择 Runner 目标。

    Select Runner in the Xcode project navigator, then select the Runner target in the settings view sidebar.

  3. 在 Identity 部分,更新 Version 为你想要发布的用户可见的版本号。

    In the Identity section, update the Version to the user-facing version number you wish to publish.

  4. 在 Identity 部分,更新 Build 标示为一个唯一的 Build 数字,用来在 App Store Connect 上追踪,每一个上传都需要一个独立的 Build 数字。

    In the Identity section, update the Build identifier to a unique build number used to track this build on App Store Connect. Each upload requires a unique build number.

最后,创建一个构建归档并将其上传到 App Store Connect:

Finally, create a build archive and upload it to App Store Connect:

  1. 运行 flutter build ipa 来生成一个构建归档

    Run flutter build ipa to produce a build archive.

  2. 在 Xcode 中打开 build/ios/archive/MyApp.xcarchive

    Open build/ios/archive/MyApp.xcarchive in Xcode.

  3. 点击 Validate… 按钮。如果报告了任何问题,记录下他们并重新开始一个新的构建。在你上传一个归档前,可以一直使用同一个 Build ID。

    Click the Validate App button. If any issues are reported, address them and produce another build. You can reuse the same build ID until you upload an archive.

  4. 当这个归档校验成功以后,点击 Upload to App Store…。你可以在 App Store Connect 中应用详情页面的 Activities 标签页查看你的构建状态。

    After the archive has been successfully validated, click Distribute App. You can follow the status of your build in the Activities tab of your app’s details page on App Store Connect.

当你的构建已经通过了校验,可以将你的构建通过 TestFlight 发布给你的测试人员或直接将其发布到 App Store 的时候,你会在 30 分钟内收到一封信来提醒你。

You should receive an email within 30 minutes notifying you that your build has been validated and is available to release to testers on TestFlight. At this point you can choose whether to release on TestFlight, or go ahead and release your app to the App Store.

更多信息可以查看 上传一个 App 到 App Store Connect (Upload an app to App Store Connect)

For more details, see Upload an app to App Store Connect.

使用 Codemagic 命令行工具创建一个构建归档

Create a build archive with Codemagic CLI tools

该步骤包含了在 Flutter 项目的目录下,通过终端使用 Flutter 构建命令和 Codemagic 命令行工具,构建归档并上传至 App Store 的教程。该操作可以让你完全控制分发证书和临时钥匙串,将它们与登录的进行隔离。

This step covers creating a build archive and uploading your build to App Store Connect using Flutter build commands and Codemagic CLI Tools executed in a terminal in the Flutter project directory. This allows you to create a build archive with full control of distribution certificates in a temporary keychain isolated from your login keychain.

  1. 安装 Codemagic 命令行工具:

    Install the Codemagic CLI tools:

    pip3 install codemagic-cli-tools
    
  2. 你需要使用包含 App 管理权限的 App Store Connect 账号,生成一个 App Store Connect API Key。为了使后续的命令更加简洁,你可以在环境变量中配置这些内容: issuer id、key id 以及 API key 文件。

    You’ll need to generate an App Store Connect API Key with App Manager access to automate operations with App Store Connect. To make subsequent commands more concise, set the following environment variables from the new key: issuer id, key id, and API key file.

    export APP_STORE_CONNECT_ISSUER_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
    export APP_STORE_CONNECT_KEY_IDENTIFIER=ABC1234567
    export APP_STORE_CONNECT_PRIVATE_KEY=`cat /path/to/api/key/AuthKey_XXXYYYZZZ.p8`
    
  3. 你需要导出或者创建一个 iOS 分发证书,用来签名及归档构建。

    You need to export or create an iOS Distribution certificate to code sign and package a build archive.

    如果你已经有可用的 证书,你可以使用以下命令导出私钥:

    If you have existing certificates, you can export the private keys by executing the following command for each certificate:

    openssl pkcs12 -in <certificate_name>.p12 -nodes -nocerts | openssl rsa -out cert_key
    

    你也可以使用以下命令创建新的私钥:

    Or you can create a new private key by executing the following command:

    ssh-keygen -t rsa -b 2048 -m PEM -f cert_key -q -N ""
    

    之后你便可以用命令行工具基于这个私钥创建新的 iOS 分发构建了。

    Later, you can have CLI tools automatically create a new iOS Distribution from the private key.

  4. 配置一个用于签名的临时钥匙串:

    Set up a new temporary keychain to be used for code signing:

    keychain initialize
    
  5. 从 App Store Connect 上获取签名文件:

    Fetch the code signing files from App Store Connect:

    app-store-connect fetch-signing-files $(xcode-project detect-bundle-id) \
        --platform IOS \
        --type IOS_APP_STORE \
        --certificate-key=@file:/path/to/cert_key \
        --create
    

    其中,cert_key 是你导出的 iOS 分发证书的私钥,或者是自动生成新证书的新私钥。如果 App Store Connect 中不存在这个证书,将会通过私钥创建。

    Where cert_key is either your exported iOS Distribution certificate private key or a new private key which automatically generates a new certificate. The certificate will be created from the private key if it doesn’t exist in App Store Connect.

  6. 将获取到的证书添加到你的钥匙串中:

    Now add the fetched certificates to your keychain:

    keychain add-certificates
    
  7. 更新 Xcode 项目设定,使用获取到的签名配置:

    Update the Xcode project settings to use fetched code signing profiles:

    xcode-project use-profiles
    
  8. 获取 Flutter 依赖:

    Install Flutter dependencies:

    flutter packages pub get
    
  9. 获取 CocoaPods 依赖:

    Install CocoaPods dependencies:

    find . -name "Podfile" -execdir pod install \;
    
  10. 构建 Flutter 的 iOS 项目:

    Build the Flutter the iOS project:

    flutter build ipa --release \
        --export-options-plist=$HOME/export_options.plist
    

    注意 export_options.plist 路径来源于 xcode-project use-profiles 命令的输出。

    Note that export_options.plist is the output of the xcode-project use-profiles command.

  11. 将应用发布到 App Store Connect:

    Publish the app to App Store Connect:

    app-store-connect publish \
        --path $(find $(pwd) -name "*.ipa")
    
  12. 如前文所示,记得将你的登录钥匙串设置为默认,避免设备出现认证问题:

    As mentioned earlier, don’t forget to set your login keychain as the default to avoid authentication issues with apps on your machine:

    keychain use-login
    

在 30 分钟内,你会收到一封邮件,提醒你构建已验证,可以在 TestFlight 上发布。这时你可以选择在 TestFlight 上发布,或是直接在 App Store 上发布。

You should receive an email within 30 minutes notifying you that your build has been validated and is available to release to testers on TestFlight. At this point you can choose whether to release on TestFlight, or go ahead and release your app to the App Store.

在 TestFlight 发布你的应用

Release your app on TestFlight

TestFlight allows developers to push their apps to internal and external testers. This optional step covers releasing your build on TestFlight.

  1. App Store Connect中,你的应用的详情页面跳转到 TestFlight Tab。

    Navigate to the TestFlight tab of your app’s application details page on App Store Connect.

  2. 在侧边栏选择 Internal Testing

    Select Internal Testing in the sidebar.

  3. 选择要发布给测试人员的构建,然后点击 保存

    Select the build to publish to testers, then click Save.

  4. 为每一个内部测试人员添加邮件。你可以在 App Store Connect 的 用户与角色 页面添加额外的内部用户,他们将会出现在页面顶部的下拉菜单中。

    Add the email addresses of any internal testers. You can add additional internal users in the Users and Roles page of App Store Connect, available from the dropdown menu at the top of the page.

关于更多信息,请查看 使用 TestFlight 分发应用 (Distribute an app using TestFlight (iOS, tvOS, watchOS))

For more details, see Distribute an app using TestFlight.

在 App Store 发布你的应用

Release your app to the App Store

当你准备发布你的 app 到这个世界时,跟随下面的步骤,来提交你的 App 去审核,并将其发布到 App Store。

When you’re ready to release your app to the world, follow these steps to submit your app for review and release to the App Store:

  1. 从你的 app 在 App Store Connect 的页面中的侧边栏中选择 Pricing and Availability,然后完善所有的必填信息。

    Select Pricing and Availability from the sidebar of your app’s application details page on App Store Connect and complete the required information.

  2. 从侧边栏选择状态。如果这是第一次发布这个 App,这个状态将会是 1.0 Prepare for Submission,填写所有需要填写的区域。

    Select the status from the sidebar. If this is the first release of this app, its status is 1.0 Prepare for Submission. Complete all required fields.

  3. 点击 提交审核

    Click Submit for Review.

Apple 将会在他们的审核过程结束后提醒你。你的 app 将会根据 Version Release 部分的介绍进行发布。

Apple notifies you when their app review process is complete. Your app is released according to the instructions you specified in the Version Release section.

关于更多细节,查看 通过 App Store 分发一个 App.

For more details, see Distribute an app through the App Store.

故障排除

Troubleshooting

分发你的应用 指南,提供了详细的发布应用到 App Store 过程的内容。

The Distribute your app guide provides a detailed overview of the process of releasing an app to the App Store.

构建和发布为 macOS 应用

目录

本教程将指导开发者如何在 App Store 上发布 Flutter 应用程序。

This guide provides a step-by-step walkthrough of releasing a Flutter app to the App Store.

预备工作

Preliminaries

在开始发布应用程序之前,请确保它符合苹果的 应用程序审查指南

Before beginning the process of releasing your app, ensure that it meets Apple’s App Review Guidelines.

为了将应用程序发布到 App Store,你必须先注册 苹果开发者计划。可以在 Apple 的 选择会员资格 指南中阅读更多关于各种会员资格的信息。

In order to publish your app to the App Store, you must first enroll in the Apple Developer Program. You can read more about the various membership options in Apple’s Choosing a Membership guide.

在 App Store Connect 上注册你的应用程序

Register your app on App Store Connect

App Store Connect(以前叫 iTunes Connect)上管理应用程序的生命周期。你可以定义应用程序的名称和描述、添加屏幕截图、设置定价以及管理应用程序商店和 TestFlight 的发布。

Manage your app’s life cycle on App Store Connect (formerly iTunes Connect). You define your app name and description, add screenshots, set pricing, and manage releases to the App Store and TestFlight.

注册应用程序包括两个步骤:注册一个唯一的 Bundle ID,以及在 App Store Connect 上创建应用程序记录。

Registering your app involves two steps: registering a unique Bundle ID, and creating an application record on App Store Connect.

有关 App Store Connect 的详细概述,请参阅 App Store Connect 指南

For a detailed overview of App Store Connect, see the App Store Connect guide.

注册 Bundle ID

Register a Bundle ID

每个 macOS 应用程序都与一个 Bundle ID 关联, Bundle ID 是在 Apple 注册的唯一标识。要为应用程序注册 Bundle ID,请执行以下步骤:

Every macOS application is associated with a Bundle ID, a unique identifier registered with Apple. To register a Bundle ID for your app, follow these steps:

  1. 打开开发者帐户的 App IDs 页面。

    Open the App IDs page of your developer account.

  2. 点击 + 创建一个新的 Bundle ID。

    Click + to create a new Bundle ID.

  3. 输入应用程序名称,选择 显式 App ID,然后输入 ID。

    Enter an app name, select Explicit App ID, and enter an ID.

  4. 选择应用程序使用的服务,然后点击 下一步

    Select the services your app uses, then click Continue.

  5. 在下一页中,确认应用的详细信息,然后点击 注册 来注册你的 Bundle ID。

    On the next page, confirm the details and click Register to register your Bundle ID.

在 App Store Connect 上创建应用程序记录

Create an application record on App Store Connect

在 App Store Connect 上注册你的应用程序:

Register your app on App Store Connect:

  1. 在浏览器中打开 App Store Connect

    Open App Store Connect in your browser.

  2. 在 App Store Connect 登录页上,点击 我的应用程序

    On the App Store Connect landing page, click My Apps.

  3. 点击我的应用程序页面左上角的 +,然后选择 新建应用程序

    Click + in the top-left corner of the My Apps page, then select New App.

  4. 在表单中填写应用程序详细信息。在平台部分,请确保选中了 iOS。由于 Flutter 目前不支持 tvOS,所以不要选中该项。点击 创建

    Fill in your app details in the form that appears. In the Platforms section, ensure that iOS is checked. Since Flutter does not currently support tvOS, leave that checkbox unchecked. Click Create.

  5. 从侧边栏中选择 应用程序信息,可以查看应用程序的详细信息。

    Navigate to the application details for your app and select App Information from the sidebar.

  6. 在常规信息中,选择在上一步中注册的 Bundle ID。

    In the General Information section, select the Bundle ID you registered in the preceding step.

更详细的介绍,请参阅 将应用程序添加到您的帐户]

For a detailed overview, see Add an app to your account.

检查 Xcode 项目设置

Review Xcode project settings

这一步包括检查 Xcode 工作区中最重要的设置。更详细的过程和说明,请参阅 准备应用程序分发

This step covers reviewing the most important settings in the Xcode workspace. For detailed procedures and descriptions, see Prepare for app distribution.

在 Xcode 中配置目标:

Navigate to your target’s settings in Xcode:

  1. 在 Xcode 中,打开应用程序 macos 文件夹中的 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace in your app’s macos folder.

  2. 要查看应用程序的设置,请在 Xcode 导航栏中选择 Runner 项目。然后,在主视图侧栏中,选择 Runner 目标。

    To view your app’s settings, select the Runner project in the Xcode project navigator. Then, in the main view sidebar, select the Runner target.

  3. 选择 General(常规) 选项。

    Select the General tab.

确认最重要的设置。

Verify the most important settings.

Identity(标识) 部分:

In the Identity section:

App Category(应用类别)
你的应用将出现在 Mac App Store 中的哪个类别,此项不能为空。

App Category
The app category under which your app will be listed on the Mac App Store. This cannot be none.

Bundle Identifier
你在 App Store Connect 注册的应用程序 ID。

Bundle Identifier
The App ID you registered on App Store Connect.

Deployment info(部署信息) 部分:

In the Deployment info section:

Deployment Target(部署目标)
应用程序支持的最低 macOS 版本。Flutter 支持 macOS 10.11 及更高版本。

Deployment Target
The minimum macOS version that your app supports. Flutter supports macOS 10.11 and later.

Signing & Capabilities(签名和功能) 部分:

In the Signing & Capabilities section:

Automatically manage signing(自动管理签名)
Xcode 是否自动管理应用程序签名和配置。默认为 true,这对于大多数应用程序来说应该足够。更复杂的场景,请参阅 代码签名指南

Automatically manage signing
Whether Xcode should automatically manage app signing and provisioning. This is set true by default, which should be sufficient for most apps. For more complex scenarios, see the Code Signing Guide.

Team(团队)
选择与你注册的 Apple 开发者帐户关联的团队。如果需要,请选择 Add Account…(添加账户…),然后更新此设置。

Team
Select the team associated with your registered Apple Developer account. If required, select Add Account…, then update this setting.

项目设置的 General(常规) 选项应类似于以下内容:

The General tab of your project settings should resemble the following:

Xcode Project Settings

有关应用程序签名的详细概述,请参阅 创建、导出和删除签名证书

For a detailed overview of app signing, see Create, export, and delete signing certificates.

Configuring the app’s name, bundle identifier and copyright

引用标识的配置集中在 macos/Runner/Configs/AppInfo.xcconfig 文件中。想修改应用名称,设置 PRODUCT_NAME;想修改版权信息,设置 PRODUCT_COPYRIGHT;想修改 Bundle ID,设置 PRODUCT_BUNDLE_IDENTIFIER

The configuration for the product identifiers are centralized in macos/Runner/Configs/AppInfo.xcconfig. For the app’s name, set PRODUCT_NAME, for the copyright set PRODUCT_COPYRIGHT, and finally set PRODUCT_BUNDLE_IDENTIFIER for the app’s bundle identifier.

更新应用程序的版本号

Updating the app’s version number

应用程序的默认版本号为 1.0.0。如需更新版本号,在 pubspec.yaml 文件中更新以下位置:

The default version number of the app is 1.0.0. To update it, navigate to the pubspec.yaml file and update the following line:

version: 1.0.0+1

版本号是三个用点分隔的数字,如上面示例中的 1.0.0,后面用 + 分隔的是可选的内部版本号,如上面示例中的 1

The version number is three numbers separated by dots, such as 1.0.0 in the example above, followed by an optional build number such as 1 in the example above, separated by a +.

版本号和内部版本号都可以在 Flutter 构建时,通过指定 --build name--build number 进行覆盖。

Both the version and the build number may be overridden in Flutter’s build by specifying --build-name and --build-number, respectively.

在 macOS 中,build-name 使用 CFBundleShortVersionString,而 build-number 使用 CFBundleVersion。在苹果开发者的网站上,查看更多关于 iOS 版本的 Core Foundation Keys

In macOS, build-name uses CFBundleShortVersionString while build-number uses CFBundleVersion. Read more about iOS versioning at Core Foundation Keys on the Apple Developer’s site.

添加应用程序图标

Add an app icon

创建一个新的 Flutter 应用程序时,会创建一个占位图标集。此步骤包含如何用应用程序的图标替换这些占位图标:

When a new Flutter app is created, a placeholder icon set is created. This step covers replacing these placeholder icons with your app’s icons:

  1. 查看 macOS 应用程序图标 指南。

    Review the macOS App Icon guidelines.

  2. 在 Xcode 项目导航栏的 Runner 文件夹中选择 Assets.xcassets。用你自己的应用程序图标更新占位图标。

    In the Xcode project navigator, select Assets.xcassets in the Runner folder. Update the placeholder icons with your own app icons.

  3. 使用 flutter run -d macos 运行应用程序,验证图标是否已被替换。

    Verify the icon has been replaced by running your app using flutter run -d macos.

创建构建存档

Create a build archive with Xcode

此步骤包含创建构建存档并将其上传到 App Store Connect。

This step covers creating a build archive and uploading your build to App Store Connect using Xcode.

在开发时,你已经完成了在 debug 模式下的应用构建、调试和测试。当你准备好在 App Store 或 TestFlight 上向用户发布应用时,你需要准备一个 release 版产物。此时,你可以考虑 混淆你的 Dart 代码 让逆向工程变得更加困难。混淆你的代码需要向构建命令添加两个标志。

During development, you’ve been building, debugging, and testing with debug builds. When you’re ready to ship your app to users on the App Store or TestFlight, you need to prepare a release build. At this point, you might consider obfuscating your Dart code to make it more difficult to reverse engineer. Obfuscating your code involves adding a couple flags to your build command.

在 Xcode 中,配置应用程序版本和内部版本:

In Xcode, configure the app version and build:

  1. 在 Xcode 中,打开应用程序 macos 文件夹中的 Runner.xcworkspace

    In Xcode, open Runner.xcworkspace in your app’s macos folder.

  2. 在 Xcode 项目导航栏中选择 Runner,然后在设置侧栏中选择 Runner 目标。

    Select Runner in the Xcode project navigator, then select the Runner target in the settings view sidebar.

  3. 在标识部分,将 Version(版本) 更新为要发布的版本号。

    In the Identity section, update the Version to the user-facing version number you wish to publish.

  4. 在标识部分,将 Build identifier(构建标识) 更新为在 App Store Connect 上可以跟踪此生成的唯一生成串。每次上传都需要一个唯一的构建标识。

    In the Identity section, update the Build identifier to a unique build number used to track this build on App Store Connect. Each upload requires a unique build number.

最后,创建一个构建存档并将其上传到 App Store Connect:

Finally, create a build archive and upload it to App Store Connect:

  1. 打开 Xcode 并选择 Product(项目)> Archive(存档)。运行 flutter build macos 生成构建存档。

    Open Xcode and select Product > Archive. Run flutter build macos to produce a build archive.

  2. 点击 验证应用程序 按钮。如果报告了任何问题,请解决并再次构建。在上传存档之前,可以重用相同的构建 ID。

    Click the Validate App button. If any issues are reported, address them and produce another build. You can reuse the same build ID until you upload an archive.

  3. 成功验证存档后,点击 分发应用程序。你可以在 App Store Connect 上的应用程序详细信息页的活动标签下查看构建状态。

    After the archive has been successfully validated, click Distribute App. You can follow the status of your build in the Activities tab of your app’s details page on App Store Connect.

你应该会在 30 分钟内收到一封邮件。告知你的构建已经过验证,可以在 TestFlight 上发布给测试人员。此时,你可以选择在 TestFlight 上发布,或者继续将应用程序发布到应用程序商店。

You should receive an email within 30 minutes notifying you that your build has been validated and is available to release to testers on TestFlight. At this point you can choose whether to release on TestFlight, or go ahead and release your app to the App Store.

更多详细信息,请参阅 将应用程序上传到 App Store Connect

For more details, see Upload an app to App Store Connect.

使用 Codemagic 命令行工具创建一个构建归档

Create a build archive with Codemagic CLI tools

下面的步骤,我们会介绍在 Flutter 应用的工程目录下执行 Flutter 构建命令和 Codemagic 命令行工具,创建一个构建归档并将其上传至 App Store Connect。

This step covers creating a build archive and uploading your build to App Store Connect using Flutter build commands and Codemagic CLI Tools executed in a terminal in the Flutter project directory.

  1. 安装 Codemagic 命令行工具:

    Install the Codemagic CLI tools:

    pip3 install codemagic-cli-tools
    
  2. 你需要生成一个具有 App Manager 访问权限的 App Store Connect API 密钥,以方便对 App Store Connect 进行自动化操作。为了使后续的命令更简洁,请设置下面的环境变量:发行者 ID、密钥 ID、API 密钥文件:

    You’ll need to generate an App Store Connect API Key with App Manager access to automate operations with App Store Connect. To make subsequent commands more concise, set the following environment variables from the new key: issuer id, key id, and API key file.

    export APP_STORE_CONNECT_ISSUER_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
    export APP_STORE_CONNECT_KEY_IDENTIFIER=ABC1234567
    export APP_STORE_CONNECT_PRIVATE_KEY=`cat /path/to/api/key/AuthKey_XXXYYYZZZ.p8`
    
  3. 你需要导出或者创建 Mac App Distribution 和 Mac Installer Distribution 证书,以便与执行代码签名以及打包构建归档。

    You need to export or create a Mac App Distribution and a Mac Installer Distribution certificate to perform code signing and package a build archive.

    对于已有的 证书,你可以选择通过下吗的命令来导出私钥:

    If you have existing certificates, you can export the private keys by executing the following command for each certificate:

    openssl pkcs12 -in <certificate_name>.p12 -nodes -nocerts | openssl rsa -out cert_key
    

    或者通过以下命令创建一个新的私钥:

    Or you can create a new private key by executing the following command:

    ssh-keygen -t rsa -b 2048 -m PEM -f cert_key -q -N ""
    

    之后,你可以让命令行工具自动创建新的 Mac App Distribution 和 Mac Installer Distribution 证书,每个新的证书都可以使用相同的私钥。

    Later, you can have CLI tools automatically create a new Mac App Distribution and Mac Installer Distribution certificate. You can use the same private key for each new certificate.

  4. 从 App Store Connect 获取需要代码签名的文件:

    Fetch the code signing files from App Store Connect:

    app-store-connect fetch-signing-files YOUR.APP.BUNDLE_ID \
        --platform MAC_OS \
        --type MAC_APP_STORE \
        --certificate-key=@file:/path/to/cert_key \
        --create
    

    上面代码里的 cert_key 是你已导出的或者新生成的 Mac App Distribution 证书私钥。

    Where cert_key is either your exported Mac App Distribution certificate private key or a new private key which automatically generates a new certificate.

  5. 如果你还没有 Mac Installer Distribution 证书,通过执行下面的命令行可以生成一个:

    If you do not have a Mac Installer Distribution certificate, you can create a new certificate by executing the following:

    app-store-connect create-certificate \
        --type MAC_INSTALLER_DISTRIBUTION \
        --certificate-key=@file:/path/to/cert_key \
        --save
    

    使用你之前创建的私钥的 cert_key

    Use cert_key of the private key you created earlier.

  6. 获取 Mac 安装程序分发证书:

    Fetch the Mac Installer Distribution certificates:

    app-store-connect list-certificates \
        --type MAC_INSTALLER_DISTRIBUTION \
        --certificate-key=@file:/path/to/cert_key \
        --save
    
  7. 设置用于代码签名的新临时钥匙串:

    Set up a new temporary keychain to be used for code signing:

    keychain initialize
    
  8. 现在将获取的证书添加到你的钥匙串中:

    Now add the fetched certificates to your keychain:

    keychain add-certificates
    
  9. 更新 Xcode 项目设置以使用获取的代码签名配置文件:

    Update the Xcode project settings to use fetched code signing profiles:

    xcode-project use-profiles
    
  10. 安装 Flutter 依赖项:

    Install Flutter dependencies:

    flutter packages pub get
    
  11. 安装 CocoaPods 依赖项:

    Install CocoaPods dependencies:

    find . -name "Podfile" -execdir pod install \;
    
  12. 启用 Flutter macOS 选项:

    Enable the Flutter macOS option:

    flutter config --enable-macos-desktop
    
  13. 构建 Flutter macOS 项目:

    Build the Flutter macOS project:

    flutter build macos --release
    
  14. 打包应用程序:

    Package the app:

    APP_NAME=$(find $(pwd) -name "*.app")
    PACKAGE_NAME=$(basename "$APP_NAME" .app).pkg
    xcrun productbuild --component "$APP_NAME" /Applications/ unsigned.pkg
    
    INSTALLER_CERT_NAME=$(keychain list-certificates \
              | jq '.[0]
                | select(.common_name
                | contains("Mac Developer Installer"))
                | .common_name' \
              | xargs)
    xcrun productsign --sign "$INSTALLER_CERT_NAME" unsigned.pkg "$PACKAGE_NAME"
    rm -f unsigned.pkg 
    
  15. 将打包的应用发布到 App Store Connect:

    Publish the packaged app to App Store Connect:

    app-store-connect publish \
        --path "$PACKAGE_NAME"
    
  16. 如前所述,不要忘记将你的登录钥匙串设置为默认设置,以避免你机器上的应用程序出现身份验证问题:

    As mentioned earlier, don’t forget to set your login keychain as the default to avoid authentication issues with apps on your machine:

    keychain use-login
    

分发到已注册的设备

Distribute to registered devices

TestFlight 不可用于向内部和外部的测试人员分发 macOS 应用。请参阅 分发指南,准备一个归档文件,以便分发到指定的 Mac 设备。

TestFlight is not available for distributing macOS apps to internal and external testers. See distribution guide to prepare an archive for distribution to designated Mac computers.

将应用程序发布到应用程序商店

Release your app to the App Store

当你准备向全世界发布应用程序时,请按照以下步骤提交应用程序以供审阅并发布到应用程序商店:

When you’re ready to release your app to the world, follow these steps to submit your app for review and release to the App Store:

  1. App Store Connect 应用程序详情页的侧栏中选择 定价和可用性,并完善相关信息。

    Select Pricing and Availability from the sidebar of your app’s application details page on App Store Connect and complete the required information.

  2. 从侧边栏中选择状态。如果这是应用程序的第一个版本,则其状态为 1.0 准备提交。填写所有必填字段。

    Select the status from the sidebar. If this is the first release of this app, its status is 1.0 Prepare for Submission. Complete all required fields.

  3. 点击 提交审核

    Click Submit for Review.

苹果会在你的应用程序审核完成后通知你。应用程序是按照你在 版本发布 说明中发布的。

Apple notifies you when their app review process is complete. Your app is released according to the instructions you specified in the Version Release section.

更多详细信息,请参阅 通过应用程序商店分发应用程序.

For more details, see Distribute an app through the App Store.

故障排除

Troubleshooting

分发你的应用程序 指南详细概述了将应用程序发布到应用商店的过程。

The Distribute your app guide provides a detailed overview of the process of releasing an app to the App Store.

构建和发布为 Linux 应用到 Snap Store

目录

During a typical development cycle, you test an app using flutter run at the command line, or by using the Run and Debug options in your IDE. By default, Flutter builds a debug version of your app.

When you’re ready to prepare a release version of your app, for example to publish to the Snap Store, this page can help.

Prerequisites

To build and publish to the Snap Store, you need the following components:

Set up the build environment

Use the following instructions to set up your build environment.

Install snapcraft

At the command line, run the following:

$ sudo snap install snapcraft --classic

Install Multipass

Also at the command line, run the following:

$ sudo snap install multipass --classic

To work correctly, Multipass requires access to the CPU virtualization extensions. If the extensions are not available for your CPU architecture, not enabled in BIOS, or not accessible (for instance if you are running a virtual machine without nested virtualization), you won’t be able to use Multipass.

If you see the following error, you should use LXD:

launch failed: CPU does not support KVM extensions

Install LXD

To install LXD, use the following command:

$ sudo snap install lxd

LXD can be used as an alternative backend during the snap build process. Once installed, LXD needs to be configured for use. The default answers are suitable for most use cases.

$ sudo lxd init
Would you like to use LXD clustering? (yes/no) [default=no]:
Do you want to configure a new storage pool? (yes/no) [default=yes]:
Name of the new storage pool [default=default]:
Name of the storage backend to use (btrfs, dir, lvm, zfs, ceph) [default=zfs]:
Create a new ZFS pool? (yes/no) [default=yes]:
Would you like to use an existing empty disk or partition? (yes/no) [default=no]:
Size in GB of the new loop device (1GB minimum) [default=5GB]:
Would you like to connect to a MAAS server? (yes/no) [default=no]:
Would you like to create a new local network bridge? (yes/no) [default=yes]:
What should the new bridge be called? [default=lxdbr0]:
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
Would you like LXD to be available over the network? (yes/no) [default=no]:
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:

On the first run, LXD may not be able to connect to its socket:

An error occurred when trying to communicate with the 'LXD'
provider: cannot connect to the LXD socket
('/var/snap/lxd/common/lxd/unix.socket').

This means you need to add your user name to the LXD (lxd) group, so log out of your session and then log back in:

$ sudo usermod -a -G lxd <your username>

Overview of snapcraft

The snapcraft tool builds snaps based on the instructions listed in a snapcraft.yaml file. To get a basic understanding of snapcraft and its core concepts, take a look at the Snap documentation and the Introduction to snapcraft. Additional links and information are listed at the bottom of this page.

Flutter snapcraft.yaml example

Place the YAML file in your Flutter project under <project root>/snap/snapcraft.yaml. (And remember that YAML files are sensitive to white space!) For example:

name: super-cool-app
version: 0.1.0
summary: Super Cool App
description: Super Cool App that does everything!

confinement: strict
base: core18
grade: stable

slots:
  dbus-super-cool-app: # adjust accordingly to your app name
    interface: dbus
    bus: session
    name: org.bar.super_cool_app # adjust accordingly to your app name and
    
apps:
  super-cool-app:
    command: super_cool_app
    extensions: [flutter-master] # Where "master" defines which Flutter channel to use for the build
    plugs:
    - network
    slots:
      - dbus-super-cool-app
parts:
  super-cool-app:
    source: .
    plugin: flutter
    flutter-target: lib/main.dart # The main entry-point file of the application

The following sections explain the various pieces of the YAML file.

Metadata

This section of the snapcraft.yaml file defines and describes the application. The snap version is derived (adopted) from the build section.

name: super-cool-app
version: 0.1.0
summary: Super Cool App
description: Super Cool App that does everything!

Grade, confinement, and base

This section defines how the snap is built.

confinement: strict
base: core18
grade: stable
Grade
Specifies the quality of the snap; this is relevant for the publication step later.
Confinement
Specifies what level of system resource access the snap will have once installed on the end-user system. Strict confinement limits the application access to specific resources (defined by plugs in the app section).
Base
Snaps are designed to be self-contained applications, and therefore, they require their own private core root filesystem known as base. The base keyword specifies the version used to provide the minimal set of common libraries, and mounted as the root filesystem for the application at runtime.

Apps

This section defines the application(s) that exist inside the snap. There can be one or more applications per snap. This example has a single application—super_cool_app.

apps:
  super-cool-app:
    command: super_cool_app
    extensions: [flutter-master]
Command
Points to the binary, relative to the snap’s root, and runs when the snap is invoked.
Extensions
A list of one or more extensions. Snapcraft extensions are reusable components that can expose sets of libraries and tools to a snap at build and runtime, without the developer needing to have specific knowledge of included frameworks. The flutter-master extension exposes the GTK 3 libraries to the Flutter snap. This ensures a smaller footprint and better integration with the system.

The flutter-master extension sets your flutter channel to master. If you would like to build your app with the dev channel simply use the flutter-dev extension.

Plugs
A list of one or more plugs for system interfaces. These are required to provide necessary functionality when snaps are strictly confined. This Flutter snap needs access to the network.
DBus interface
The DBus interface provides a way for snaps to communicate over DBus. The snap providing the DBus service declares a slot with the well-known DBus name and which bus it uses. Snaps wanting to communicate with the providing snap’s service declare a plug for the providing snap. Note that a snap declaration is needed for your snap to be delivered via the snap store and claim this well-known DBus name (simply upload the snap to the store and request a manual review and a reviewer will take a look).

When a providing snap is installed, snapd will generate security policy that will allow it to listen on the well-known DBus name on the specified bus. If the system bus is specified, snapd will also generate DBus bus policy that allows ‘root’ to own the name and any user to communicate with the service. Non-snap processes are allowed to communicate with the providing snap following traditional permissions checks. Other (consuming) snaps may only communicate with the providing snap by connecting the snaps’ interface.

dbus-super-cool-app: # adjust accordingly to your app name
  interface: dbus
  bus: session
  name: dev.site.super_cool_app 

Parts

This section defines the sources required to assemble the snap.

Parts can be downloaded and built automatically using plugins. Similar to extensions, snapcraft can use various plugins (like Python, C, Java, Ruby, etc) to assist in the building process. Snapcraft also has some special plugins.

nil plugin
Performs no action and the actual build process is handled using a manual override.
flutter plugin
Provides the necessary Flutter SDK tools so you can use it without having to manually download and set up the build tools.
parts:
  super-cool-app:
    source: .
    plugin: flutter
    flutter-target: lib/main.dart # The main entry-point file of the application

Desktop file and icon

Desktop entry files are used to add an application to the desktop menu. These files specify the name and icon of your application, the categories it belongs to, related search keywords and more. These files have the extension .desktop and follow the XDG Desktop Entry Specification version 1.1.

Flutter super-cool-app.desktop example

Place the .desktop file in your Flutter project under <project root>/snap/gui/super-cool-app.desktop.

Notice: icon and .desktop file name must be the same as your app name in yaml file!

For example:

  [Desktop Entry]
  Name=Super Cool App
  Comment=Super Cool App that does everything
  Exec=super-cool-app 
  Icon=${SNAP}/meta/gui/super-cool-app.png # replace name to your app name
  Terminal=false
  Type=Application
  Categories=Education; #adjust accordingly your snap category

Place your icon with .png extension in your Flutter project under <project root>/snap/gui/super-cool-app.png.

Build the snap

Once the snapcraft.yaml file is complete, run snapcraft as follows from the root directory of the project.

To use the Multipass VM backend:

$ snapcraft

To use the LXD container backend:

$ snapcraft --use-lxd

Publish

Once the snap is built, you’ll have a <name>.snap file in your root project directory. You can now publish the snap. The process consists of the following:

  1. Create a developer account at snapcraft.io, if you haven’t already done so.
  2. Register the app’s name. Registration can be done either using the Snap Store Web UI portal, or from the command line, as follows:
    $ snapcraft login
    $ snapcraft register
    
  3. Release the app. After reading the next section to learn about selecting a Snap Store channel, push the snap to the store:
    $ snapcraft upload --release=<channel> <file>.snap
    

Snap Store channels

The Snap Store uses channels to differentiate among different versions of snaps.

The snapcraft upload command uploads the snap file to the store. However, before you run this command, you need to learn about the different release channels. Each channel consists of three components:

Track
All snaps must have a default track called latest. This is the implied track unless specified otherwise.
Risk
Defines the readiness of the application. The risk levels used in the snap store are: stable, candidate, beta, and edge.
Branch
Allows creation of short-lived snap sequences to test bug-fixes.

Snap Store automatic review

The Snap Store runs several automated checks against your snap. There may also be a manual review, depending on how the snap was built, and if there are any specific security concerns. If the checks pass without errors, the snap becomes available in the store.

Additional resources

You can learn more from the following links on the snapcraft.io site:

构建和发布为 Web 应用

目录

(例如)在典型的开发过程中,你可以在命令行使用 flutter run -d chrome 命令测试应用程序。这会构建出 debug 版本的应用。

During a typical development cycle, you test an app using flutter run -d chrome (for example) at the command line. This builds a debug version of your app.

本页面会帮助你构建 release 版本的应用,其囊括了如下主题:

This page helps you prepare a release version of your app and covers the following topics:

处理 Web 中的图片

Handling images on the web

Web 支持标准的 Image widge 来显示图片。但是,由于 Web 浏览器需要安全地运行不受信任的代码,因此与移动和桌面平台相比,图像处理方面存在某些限制。

The web supports the standard Image widget to display images. However, because web browsers are built to run untrusted code safely, there are certain limitations in what you can do with images compared to mobile and desktop platforms.

更多信息,请参阅 在 Web 中展示图片.

For more information, see Displaying images on the web.

选择 Web 渲染器

Choosing a web renderer

默认情况下,flutter buildflutter run 命令对 Web 渲染器使用 auto 参数。这意味着您的应用程序在移动浏览器上会与 HTML 渲染器一起运行,而在桌面浏览器上与 CanvasKit 一起运行。这是我们推荐的组合方式,能够针对每个平台特性优化。

By default, the flutter build and flutter run commands use the auto choice for the web renderer. This means that your app runs with the HTML renderer on mobile browsers and CanvasKit on desktop browsers. This is our recommended combination to optimize for the characteristics of each platform.

更多信息,请参阅 Web 渲染器.

For more information, see Web renderers.

混淆并压缩代码

Minification

当你创建了一个 release 版本时,便已经压缩了代码。

Minification is handled for you when you create a release build.

Debug 模式构建的 Web 应用没有被压缩,且 Tree-shaking 没有执行。

A debug build of a web app is not minified and tree shaking has not been performed.

Profile 模式构建的 Web 应用没有被压缩,但 Tree-shaking 执行了。

A profile build is not minified and tree shaking has been performed.

Release 模式构建的 Web 应用被压缩了,并且 Tree-shaking 执行了。

A release build is both minified and tree shaking has been performed.

构建用于发布的应用

Building the app for release

使用 flutter build web 命令构建应用程序以进行部署。你也可以通过使用 --web-renderer 自行选择渲染方式。(请查看 网页渲染器)这将生成包括资源的应用程序,并将文件放入项目的 /build/web 目录中。

Build the app for deployment using the flutter build web command. You can also choose which renderer to use by using the --web-renderer option (See Web renderers). This generates the app, including the assets, and places the files into the /build/web directory of the project.

一般的应用程序的 release 版本具有以下结构:

The release build of a simple app has the following structure:

/build/web
  assets
    AssetManifest.json
    FontManifest.json
    NOTICES
    fonts
      MaterialIcons-Regular.ttf
      <other font files>
    <image files>
  index.html
  main.dart.js
  main.dart.js.map

启动 Web 服务器(例如,python -m SimpleHTTPServer 8000,或使用 dhttpd package),然后打开 /build/web 目录。在浏览器中访问 localhost:8000(前文用 Python 启动的服务器)以查看应用程序的 release 版本。

Launch a web server (for example, python -m http.server 8000, or by using the dhttpd package), and open the /build/web directory. Navigate to localhost:8000 in your browser (given the python SimpleHTTPServer example) to view the release version of your app.

将 Flutter 应用内嵌到一个 HTML 页面里

Embedding a Flutter app into an HTML page

你可以使用 iframe 标签将 Flutter web 应用内嵌到一个网页里。请参照下面的例子,将 URL 替换成实际的地址:

You can embed a Flutter web app, as you would embed other content, in an iframe tag of an HTML file. In the following example, replace “URL” with the location of your HTML page:

<iframe src="URL"></iframe>

部署到 Web 端

Deploying to the web

等你准备好部署应用时,将 release 包上传到 Firebase、云或者是类似服务上:

When you are ready to deploy your app, upload the release bundle to Firebase, the cloud, or a similar service. Here are a few possibilities, but there are many others:

PWA Support

从 1.20 版开始,用于 Web 应用程序的 Flutter 模板包括了对可安装且具有离线功能的 PWA 应用程序所需的核心功能的支持。基于 Flutter 的 PWA 的安装方式与其他基于 Web 的 PWA 基本相同;由 manifest.json 提供的配置信息可以声明您的 Flutter 应用程序是 PWA,该文件可以在 web 目录中使用 Flutter create 命令生成。

As of release 1.20, the Flutter template for web apps includes support for the core features needed for an installable, offline-capable PWA app. Flutter-based PWAs can be installed in the same way as any other web-based PWA; the settings signaling that your Flutter app is a PWA are provided by manifest.json, which is produced by flutter create in the web directory.

对 PWA 的支持仍在进行中,因此,如果您发现不正确的地方,欢迎 给予我们反馈

PWA support remains a work in progress, so please give us feedback if you see something that doesn’t look right.

Flutter 里的持续部署

目录

通过 Flutter 持续交付的最佳实践,确保您的应用程序交付给您的 Beta 版本测试人员并能够频繁予以验证,而无需借助手动工作流程。

Follow continuous delivery best practices with Flutter to make sure your application is delivered to your beta testers and validated on a frequent basis without resorting to manual workflows.

CI/CD 选择

CI/CD Options

有许多持续集成 (CI) 和持续交付 (CD) 的工具,帮助自动发布你的应用。

There are a number of continuous integration (CI) and continuous delivery (CD) options available to help automate the delivery of your application.

内置 Flutter 的多合一 (All-in-one) 选择:

All-in-one options with built-in Flutter functionality

使用 Fastlane 与现有工作流程集成

Integrating fastlane with existing workflows

你可以通过下面的工具使用 fastlane:

You can use fastlane with the following tooling:

这份指南展示了如何让设置 fastlane 以及将其集成到现有应用的测试和持续集成 (CI) 工作流当中去。更多相关的内容,请参考上面这部分的内容。

This guide shows how to set up fastlane and then integrate it with your existing testing and continuous integration (CI) workflows. For more information, see “Integrating fastlane with existing workflow”.

fastlane

fastlane 是一个开源工具套件,帮助你自动的打包正式版以及部署你的应用。

fastlane is an open-source tool suite to automate releases and deployments for your app.

本地设置

Local setup

建议在迁移到基于云计算的系统之前,先在本地测试其构建和部署流程。您还可以使用本地机器执行连续交付。

It’s recommended that you test the build and deployment process locally before migrating to a cloud-based system. You could also choose to perform continuous delivery from a local machine.

  1. 安装 fastlane gem install fastlanebrew install fastlane。访问 fastlane docs 以获得更多信息。

    Install fastlane gem install fastlane or brew install fastlane. Visit the fastlane docs for more info.

  2. 创建一个名为 FLUTTER_ROOT 的环境变量,并将其设置为 Flutter SDK 的根目录。(这是为 iOS 部署的脚本所必需的。)

    Create an environment variable named FLUTTER_ROOT, and set it to the root directory of your Flutter SDK. (This is required for the scripts that deploy for iOS.)

  3. 创建您的 Flutter 项目,准备就绪后,确保通过如下途径构建项目:

    Create your Flutter project, and when ready, make sure that your project builds via

    • Android flutter build appbundle;
    • iOS flutter build ios --release --no-codesign.
  4. 初始化各平台的 fastlane 项目:

    Initialize the fastlane projects for each platform.

    • Android:在 [project]/android 目录中,运行 fastlane init 命令。

      Android In your [project]/android directory, run fastlane init.

    • iOS:在 [project]/ios 目录下,运行 fastlane init 命令。

      iOS In your [project]/ios directory, run fastlane init.

  5. 编辑 Appfile 以确保它有应用程序的基本数据配置:

    Edit the Appfiles to ensure they have adequate metadata for your app.

    • Android 检查在 [project]/android/fastlane/Appfile 文件中的 package_name 是否匹配在 AndroidManifest.xml 中的包名。

      Android Check that package_name in [project]/android/fastlane/Appfile matches your package name in AndroidManifest.xml.

    • iOS 检查在 [project]/ios/fastlane/Appfile 中的 app_identifier 是否匹配 Info.plist 文件中的 bundle identifier。将相应的 apple_iditc_team_idteam_id 输入进去。

      iOS Check that app_identifier in [project]/ios/fastlane/Appfile also matches Info.plist’s bundle identifier. Fill in apple_id, itc_team_id, team_id with your respective account info.

  6. 设置应用商店的本地登录凭据。

    Set up your local login credentials for the stores.

    • Android 按照 Supply setup steps 文档操作,并且确保 fastlane supply init 成功同步了你在 Google Play 商店控制台中的数据。 .json 文件与密码一样重要,切勿将其公开在任何公共源代码控制存储库。

      Android Follow the Supply setup steps and ensure that fastlane supply init successfully syncs data from your Play Store console. Treat the .json file like your password and do not check it into any public source control repositories.

    • iOS iTunes Connect 用户名已经存在于您的 Appfileapple_id 字段中,你需要将你的 iTunes 密码设置到 FASTLANE_PASSWORD 这个环境变量里。否则,上传到 iTunes/TestFlight时会提示你。

      iOS Your iTunes Connect username is already in your Appfile’s apple_id field. Set the FASTLANE_PASSWORD shell environment variable with your iTunes Connect password. Otherwise, you’ll be prompted when uploading to iTunes/TestFlight.

  7. 设置代码签名:

    Set up code signing.

    • Android 参考文档 为应用签名

      Android Follow the Android app signing steps.

    • iOS 在iOS上,当您准备使用 TestFlight 或 App Store 进行测试和部署时,使用分发证书而不是开发证书进行创建和签名。

      iOS On iOS, create and sign using a distribution certificate instead of a development certificate when you’re ready to test and deploy using TestFlight or App Store.

      • Apple Developer Account console 创建并下载一个分发证书。

        Create and download a distribution certificate in your Apple Developer Account console.

      • 打开 [project]/ios/Runner.xcworkspace/ 在你的项目设置里选择一个分发证书。

        open [project]/ios/Runner.xcworkspace/ and select the distribution certificate in your target’s settings pane.

  8. 给每个不同的平台创建一个 Fastfile 脚本。

    Create a Fastfile script for each platform.

    • Android 在 Android 上按照 fastlane Android beta deployment guide 指引操作。你可以简单的编辑一下文件,加一个名叫 upload_to_play_storelane。为了使用 flutter build 命令编译 aab,要把 apk 参数设置为 ../build/app/outputs/bundle/release/app-release.aab

      Android On Android, follow the fastlane Android beta deployment guide. Your edit could be as simple as adding a lane that calls upload_to_play_store. Set the aab argument to ../build/app/outputs/bundle/release/app-release.aab to use the app bundle flutter build already built.

    • iOS 在 iOS 上,按照 fastlane iOS beta 部署指南 指引操作。你可以简单编辑一下文件,加一个名叫 build_ios_applane,并且同时调用 export_method: 'app-store'upload_to_testflight。在 iOS 上只有当要编译成 .app 的时候才会用到 flutter build,其他情况用不到。

      iOS On iOS, follow the fastlane iOS beta deployment guide. Your edit could be as simple as adding a lane that calls build_ios_app with export_method: 'app-store' and upload_to_testflight. On iOS an extra build is required since flutter build builds an .app rather than archiving .ipas for release.

你现在已准备好在本地执行部署或将部署过程迁移到持续集成(CI)系统。

You’re now ready to perform deployments locally or migrate the deployment process to a continuous integration (CI) system.

在本地运行部署

Running deployment locally

  1. 构建发布模式的应用:

    Build the release mode app.

    • Android flutter build appbundle.
    • iOS flutter build ios --release --no-codesign.

    这个时候不用签名,接下来 fastlane 会自动签名。

    No need to sign now since fastlane will sign when archiving.

  2. 在每个平台上运行 Fastfile 脚本。

    Run the Fastfile script on each platform.

    • Android cd android then fastlane [name of the lane you created].
    • iOS cd ios then fastlane [name of the lane you created].

云构建和部署设置

Cloud build and deploy setup

首先,按照“本地设置”中描述的本地设置部分,确保在迁移到 Travis 等云系统之前,该过程有效。

First, follow the local setup section described in ‘Local setup’ to make sure the process works before migrating onto a cloud system like Travis.

需要考虑的主要事项是,由于云实例是短暂且不可信的,因此你不能在服务器上保留你的凭据,如 Play Store 服务帐户 JSON 或 iTunes 分发证书。

The main thing to consider is that since cloud instances are ephemeral and untrusted, you won’t be leaving your credentials like your Play Store service account JSON or your iTunes distribution certificate on the server.

持续集成 (CI) 系统通常支持加密的环境变量来存储私有数据。你可以使用 --dart-define MY_VAR=MY_VALUE 在构建应用时传递环境变量。

Continuous Integration (CI) systems generally support encrypted environment variables to store private data. You can pass these environment variables using --dart-define MY_VAR=MY_VALUE while building the app.

采取预防措施,不要在测试脚本中将这些变量值重新回显到控制台。 在合并之前,这些变量在拉取请求中也不可用,以确保恶意行为者无法创建打印这些密钥的拉取请求。在接受和合并的 pull 请求中,请注意与这些密钥。

Take precaution not to re-echo those variable values back onto the console in your test scripts. Those variables are also not available in pull requests until they’re merged to ensure that malicious actors cannot create a pull request that prints these secrets out. Be careful with interactions with these secrets in pull requests that you accept and merge.

  1. 暂时性登录凭据。

    Make login credentials ephemeral.

    • ![Android](https://flutter.cn/docs/assets/images/docs/cd/android.png 在 Android 上:

      Android On Android:

      • Appfile 中删除 json_key_file 并将其存储在 CI 系统的加密变量里。从 Fastfile 中直接读取这些环境变量。

        Remove the json_key_file field from Appfile and store the string content of the JSON in your CI system’s encrypted variable. Read the environment variable directly in your Fastfile.

        upload_to_play_store(
          ...
          json_key_data: ENV['<variable name>']
        )
        
      • 序列化您的上传密钥(例如,使用 base64)并将其另存为加密环境变量。可以可以在安装阶段在 CI 系统上对其进行反序列化

        Serialize your upload key (for example, using base64) and save it as an encrypted environment variable. You can deserialize it on your CI system during the install phase with

        echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > [path to your upload keystore]
        
    • iOS 在 iOS 上:

      iOS On iOS:

      • 将本地环境变量 FASTLANE_PASSWORD 转而使用 CI 系统的加密的环境变量。

        Move the local environment variable FASTLANE_PASSWORD to use encrypted environment variables on the CI system.

      • CI 系统需要有权限拿到你的分发证书。建议使用fastlane 的 Match 系统在不同的机器上同步你的证书。

        The CI system needs access to your distribution certificate. fastlane’s Match system is recommended to synchronize your certificates across machines.

  2. 建议每次使用 Gemfile 而不是 gem install fastlane 以避免其在 CI 系统上使用的不确定性,以确保 fastlane 依赖关系在本地和云计算机之间稳定且可重现。但是,此步骤是可选的。

    It’s recommended to use a Gemfile instead of using an indeterministic gem install fastlane on the CI system each time to ensure the fastlane dependencies are stable and reproducible between local and cloud machines. However, this step is optional.

    • [project]/android[project]/ios 文件夹中,创建一个 Gemfile 包含以下内容:

      In both your [project]/android and [project]/ios folders, create a Gemfile containing the following content:

      source "https://rubygems.org"
      gem "fastlane"
      
    • 在两个目录中,运行 bundle update 并将两者的 GemfileGemfile.lock 文件纳入源代码管理。

      In both directories, run bundle update and check both Gemfile and Gemfile.lock into source control.

    • 当你在本地运行的时候,请使用 bundle exec fastlane 而不是 fastlane

      When running locally, use bundle exec fastlane instead of fastlane.

  3. 在你的仓库根目录创建一个 CI 测试脚本,例如: .travis.yml.cirrus.yml

    Create the CI test script such as .travis.yml or .cirrus.yml in your repository root.

    • 有关特定于 CI 的设置,请参见 fastlane CI 文档

      See fastlane CI documentation for CI specific setup.

    • 分开你的脚本以便能在 Linux 和 macOS 两个平台运行。

      Shard your script to run on both Linux and macOS platforms.

    • 在 CI 的设置阶段,执行下列内容:

      During the setup phase of the CI task, do the following:

      • 通过执行 gem install bundler 确保 Bundler 可用。

        Ensure Bundler is available using gem install bundler.

      • [project]/android[project]/ios 目录下分别运行 bundle install命令。

        Run bundle install in [project]/android or [project]/ios.

      • 确保 Flutter SDK 已经正确了设置在了 PATH 环境变量中。

        Make sure the Flutter SDK is available and set in PATH.

      • 在 Android 平台上,请确保已经设置正确的 ANDROID_SDK_ROOT 环境变量。

        For Android, ensure the Android SDK is available and the ANDROID_SDK_ROOT path is set.

      • 在 iOS 平台上,你需要为 Xcode 指定依赖 (比如: osx_image: xcode9.2)

        For iOS, you may have to specify a dependency on Xcode (for example osx_image: xcode9.2).

    • 在 CI 任务的脚本阶段:

      In the script phase of the CI task:

      • 根据平台的不同可以运行 flutter build appbundle 或者 flutter build ios --release --no-codesign

        Run flutter build appbundle or flutter build ios --release --no-codesign, depending on the platform.

      • 然后执行 cd androidcd ios 命令。

        cd android or cd ios.

      • 最后执行 bundle exec fastlane [name of the lane] 命令。

        bundle exec fastlane [name of the lane].

Dart 语言指引

初学 Dart 语言?我们准备了一些最受欢迎的资源来帮助你快速上手 Dart。很多人说,Dart 是非常简单易学的。我们希望这些资源同样能够让你学习 Dart 变得更加简单。

New to the Dart language? We compiled our favorite resources to help you quickly learn Dart. Many people have reported that Dart is easy and fun to learn. We hope these resources make Dart easy for you to learn, too.

Dart 语言学习之旅
这是对 Dart 语言最好的介绍。能够帮助你学习 Dart 的特性,比如 强类型闭包词法作用域顶级函数命名参数async / await 等等。

Language tour
Your best introduction to the Dart language. Learn about Dart’s features such as strong types, closures, libraries, lexical scoping, top-level functions, named parameters, async / await, and lots more.

Dart 库学习之旅
这是一个对 Dart 强大的核心库的概述。你能够学习到关于 Dart 对于集合、async、math、number、strings、JSON 的支持。

Library tour
A good overview of Dart’s powerful core libraries. Learn about Dart’s support for collections, async, math, numbers, strings, JSON, and more.

给 Java 开发者的 Dart 入门代码实验室
使用你的 Java 知识来快速上手 Dart。你可以在这个 Java 指南中学习关于类、构造器、参数和接口的相关案例。

Intro to Dart for Java Developers
Use your Java knowledge to get up and running quickly with Dart. Learn about classes, constructors, parameters, and interfaces with examples from the Java Tutorial.

高效 Dart 指南
在这个指南中你可以查阅关于样式、写作文档、用法等内容

Effective Dart
Guides for style, authoring documentation, usage, and more.

异步编程: futures, async, await 指南
这能够帮助你在 Dart 核心库中熟练使用 Futures。Futures 可以用来替代一次性回调。

Asynchronous programming: futures, async, await
Learn how to use Futures, which are used extensively in the Dart core libraries. Futures can be used instead of one-time callbacks.

异步编程: Streams 指南
这能够帮助你在 Dart 核心库中熟练使用 Streams。其中 Streams 可以用来替代重复使用的回调。比如,我们在 File 类 中可以使用 Streams 从文件中读取字节。

Asynchronous programming: streams
Learn how to use Streams, which are used extensively in the Dart core libraries. Streams can be used instead repeating callbacks. For example, the File class uses Streams to read bytes from a file.

如果你想了解更多信息或是想要参与贡献,欢迎查看我们的 Dart 社区

Want to learn more and perhaps contribute? Check out the Dart community.

Flutter 兼容性策略

目录

Flutter 团队努力平衡对 API 稳定性的需求和对 API 持续研发以修复 bug,提升其人机工程学体验的需求。并且我们会通过一种连贯的方式来提供新特性。

The Flutter team tries to balance the need for API stability with the need to keep evolving APIs to fix bugs, improve API ergonomics, and provide new features in a coherent manner.

为此,我们已经创建了一个测试登记。你可以在这里针对每个改动为你的应用或库提供单元测试,以帮助我们追踪对现存应用造成破坏的那些改动。我们承诺,与这些测试的开发者进行合作以确定以下两点之前,将不会有任何改动破坏这些测试。(1)决定改动是否足够有价值;(2)提供对代码的修复方案使得这些测试能够继续通过。

To this end, we have created a test registry where you can provide unit tests for your own applications or libraries that we run on every change to help us track changes that would break existing applications. Our commitment is that we won’t make any changes that break these tests without working with the developers of those tests to (a) determine if the change is sufficiently valuable, and (b) provide fixes for the code so that the tests continue to pass.

作为该计划的一部分,如果你想要提供一些测试方案,请向 flutter/tests repository 提交 PR。这个仓库中的 README.md 文件描述了具体流程。

If you would like to provide tests as part of this program, please submit a PR to the flutter/tests repository. The README.md file on that repository describes the process in detail.

公告和迁移指南

Announcements and migration guides

如果我们确实发布了一项重要改动(定义为:会导致一个或更多已提交的测试需要变化的改动),我们将通过 flutter-announce 邮箱列表公布,并且同时写在发布版说明上。

If we do make a breaking change (defined as a change that caused one or more of these submitted tests to require changes), we will announce the change on our flutter-announce mailing list as well as in our release notes.

我们提供一个受重要改动影响的 迁移代码指南 列表。

We provide a list of guides for migrating code affected by breaking changes.

废弃政策

Deprecation policy

我们将会不定期的废弃一些确定的 API,而不是直接让他们不可用。这将独立于我们的兼容性政策,只基于已提交的测试是否失败,就如同之前描述的那样。

We will, on occasion, deprecate certain APIs rather than outright break them overnight. This is independent of our compatibility policy which is exclusively based on whether submitted tests fail, as described above.

已经废弃的 API 将会在一个宽限周期后移除。以发布至稳定版本时开始至一个日历年,或是 4 个稳定版本的发布,为一个宽限周期,以时间最长者为准。

Deprecated APIs are removed after a migration grace period. This grace period is one calendar year after being released on the stable channel, or after 4 stable releases, whichever is longer.

当已经废弃的 API 到达了弃用期限时,我们会依照同上的步骤移除废弃的 API。

When a deprecation does reach end of life, we follow the same procedures listed above for making breaking changes in removing the deprecated API.

Dart 和其它被 Flutter 使用的库

Dart and other libraries used by Flutter

Dart 语言本身有另外一个重要改动政策,记录在 Dart wiki 中

The Dart language itself has a separate breaking-change policy, documented on the Dart wiki.

总而言之,关于其它依赖的重要改动,Flutter 团队目前没有做出任何承诺。例如,有可能 Flutter 的一个新版本使用了新版本的 Skia(Flutter 使用的图形引擎)或者 Harfbuzz(Flutter 使用的字体形状引擎),将会影响到已提交测试的改动。这一类的改动不一定会被写入迁移指南。

In general, the Flutter team does not currently have any commitment regarding breaking changes for other dependencies. For example, it’s possible that a new version of Flutter using a new version of Skia (the graphics engine used by Flutter) or Harfbuzz (the font shaping engine used by Flutter) would have changes that affect contributed tests. Such changes would not necessarily be accompanied by a migration guide.

Flutter 架构概览

目录

该文章旨在提供更深入的 Flutter 架构概览,包含其设计层面的核心原则及概念。

This article is intended to provide a high-level overview of the architecture of Flutter, including the core principles and concepts that form its design.

Flutter 是一个跨平台的 UI 工具集,它的设计初衷,就是允许在各种操作系统上复用同样的代码,例如 iOS 和 Android,同时让应用程序可以直接与底层平台服务进行交互。如此设计是为了让开发者能够在不同的平台上,都能交付拥有原生体验的高性能应用,尽可能地共享复用代码的同时,包容不同平台的差异。

Flutter is a cross-platform UI toolkit that is designed to allow code reuse across operating systems such as iOS and Android, while also allowing applications to interface directly with underlying platform services. The goal is to enable developers to deliver high-performance apps that feel natural on different platforms, embracing differences where they exist while sharing as much code as possible.

在开发中,Flutter 应用会在一个 VM(程序虚拟机)中运行,从而可以在保留状态且无需重新编译的情况下,热重载相关的更新。对于发行版 (release) ,Flutter 应用程序会直接编译为机器代码(Intel x64 或 ARM 指令集),或者针对 Web 平台的 JavaScript。 Flutter 的框架代码是开源的,遵循 BSD 开源协议,并拥有蓬勃发展的第三方库生态来补充核心库功能。

During development, Flutter apps run in a VM that offers stateful hot reload of changes without needing a full recompile. For release, Flutter apps are compiled directly to machine code, whether Intel x64 or ARM instructions, or to JavaScript if targeting the web. The framework is open source, with a permissive BSD license, and has a thriving ecosystem of third-party packages that supplement the core library functionality.

概览分为以下几部分内容:

This overview is divided into a number of sections:

  1. 分层模型:Flutter 的构成要素。

    The layer model: The pieces from which Flutter is constructed.

  2. 响应式用户界面:Flutter 用户界面开发的核心概念。

    Reactive user interfaces: A core concept for Flutter user interface development.

  3. widgets 介绍:构建 Flutter 用户界面的基石。

    An introduction to widgets: The fundamental building blocks of Flutter user interfaces.

  4. 渲染过程:Flutter 如何将界面布局转化为像素。

    The rendering process: How Flutter turns UI code into pixels.

  5. 平台嵌入层 的概览:让 Flutter 应用可以在移动端及桌面端操作系统执行的代码。

    An overview of the platform embedders: The code that lets mobile and desktop OSes execute Flutter apps.

  6. 将 Flutter 与其他代码进行集成:Flutter 应用可用的各项技术的更多信息。

    Integrating Flutter with other code: Information about different techniques available to Flutter apps.

  7. Web 支持:Flutter 在浏览器环境中的特性的概述。

    Support for the web: Concluding remarks about the characteristics of Flutter in a browser environment.

架构层

Architectural layers

Flutter 被设计为一个可扩展的分层系统。它可以被看作是各个独立的组件的系列合集,上层组件各自依赖下层组件。组件无法越权访问更底层的内容,并且框架层中的各个部分都是可选且可替代的。

Flutter is designed as an extensible, layered system. It exists as a series of independent libraries that each depend on the underlying layer. No layer has privileged access to the layer below, and every part of the framework level is designed to be optional and replaceable.

Architectural
diagram

对于底层操作系统而言,Flutter 应用程序的包装方式与其他原生应用相同。在每一个平台上,会包含一个特定的嵌入层,从而提供一个程序入口,程序由此可以与底层操作系统进行协调,访问诸如 surface 渲染、辅助功能和输入等服务,并且管理事件循环队列。该嵌入层采用了适合当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。 Flutter 本身包含了各个常见平台的嵌入层,同时也 存在一些其他的嵌入层

To the underlying operating system, Flutter applications are packaged in the same way as any other native application. A platform-specific embedder provides an entrypoint; coordinates with the underlying operating system for access to services like rendering surfaces, accessibility, and input; and manages the message event loop. The embedder is written in a language that is appropriate for the platform: currently Java and C++ for Android, Objective-C/Objective-C++ for iOS and macOS, and C++ for Windows and Linux. Using the embedder, Flutter code can be integrated into an existing application as a module, or the code may be the entire content of the application. Flutter includes a number of embedders for common target platforms, but other embedders also exist.

Flutter 引擎 毫无疑问是 Flutter 的核心,它主要使用 C++ 编写,并提供了 Flutter 应用所需的原语。当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。

At the core of Flutter is the Flutter engine, which is mostly written in C++ and supports the primitives necessary to support all Flutter applications. The engine is responsible for rasterizing composited scenes whenever a new frame needs to be painted. It provides the low-level implementation of Flutter’s core API, including graphics (through Skia), text layout, file and network I/O, accessibility support, plugin architecture, and a Dart runtime and compile toolchain.

引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层。该库暴露了最底层的原语,包括用于驱动输入、图形、和文本渲染的子系统的类。

The engine is exposed to the Flutter framework through dart:ui, which wraps the underlying C++ code in Dart classes. This library exposes the lowest-level primitives, such as classes for driving input, graphics, and text rendering subsystems.

通常,开发者可以通过 Flutter 框架层 与 Flutter 交互,该框架提供了以 Dart 语言编写的现代响应式框架。它包括由一系列层组成的一组丰富的平台,布局和基础库。从下层到上层,依次有:

Typically, developers interact with Flutter through the Flutter framework, which provides a modern, reactive framework written in the Dart language. It includes a rich set of platform, layout, and foundational libraries, composed of a series of layers. Working from the bottom to the top, we have:

Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camerawebview;与平台无关的功能,例如 charactershttpanimations。还有一些软件包来自于更为宽泛的生态系统中,例如 应用内支付Apple 认证Lottie 动画

The Flutter framework is relatively small; many higher-level features that developers might use are implemented as packages, including platform plugins like camera and webview, as well as platform-agnostic features like characters, http, and animations that build upon the core Dart and Flutter libraries. Some of these packages come from the broader ecosystem, covering services like in-app payments, Apple authentication, and animations.

该概览的其余部分将从 UI 开发的响应式范例开始,浏览各个构建层。而后,我们会讲述 widgets 如何被组织,并转换成应用程序的渲染对象。同时我们也会讲述 Flutter 如何在平台层面与其他代码进行交互,最终,我们会对目前 Flutter 对于 Web 平台的支持与其他平台的异同做一个总结。

The rest of this overview broadly navigates down the layers, starting with the reactive paradigm of UI development. Then, we describe how widgets are composed together and converted into objects that can be rendered as part of an application. We describe how Flutter interoperates with other code at a platform level, before giving a brief summary of how Flutter’s web support differs from other targets.

响应式用户界面

Reactive user interfaces

粗略一看,Flutter 是 一个响应式的且伪声明式的 UI 框架,开发者负责提供应用状态与界面状态之间的映射,框架则在运行时将应用状态的更改更新到界面上。这样的模型架构的灵感来自 Facebook 自己的 React 框架 ,其中包含了对传统设计理念的再度解构。

On the surface, Flutter is a reactive, pseudo-declarative UI framework, in which the developer provides a mapping from application state to interface state, and the framework takes on the task of updating the interface at runtime when the application state changes. This model is inspired by work that came from Facebook for their own React framework, which includes a rethinking of many traditional design principles.

在大部分传统的 UI 框架中,界面的初始状态通常会被一次性定义,然后,在运行时根据用户代码分别响应事件进行更新。在这里有一项大挑战,即随着应用程序的复杂性日益增长,开发者需要对整个 UI 的状态关联有整体的认知。让我们来看看如下的 UI:

In most traditional UI frameworks, the user interface’s initial state is described once and then separately updated by user code at runtime, in response to events. One challenge of this approach is that, as the application grows in complexity, the developer needs to be aware of how state changes cascade throughout the entire UI. For example, consider the following UI:

Color picker dialog

很多地方都可以更改状态:颜色框、色调滑条、单选按钮。在用户与 UI 进行交互时,状态的改变可能会影响到每一个位置。更糟糕的是,UI 的细微变动很有可能会引发无关代码的连锁反应,尤其是当开发者并未注意其关联的时候。

There are many places where the state can be changed: the color box, the hue slider, the radio buttons. As the user interacts with the UI, changes must be reflected in every other place. Worse, unless care is taken, a minor change to one part of the user interface can cause ripple effects to seemingly unrelated pieces of code.

我们可以通过类似 MVC 的方式进行处理,开发者将数据的改动通过控制器(Controller)推至模型(Model),模型再将新的状态通过控制器推至界面(View)。但这样的处理方式仍然存在问题,因为创建和更新 UI 元素的操作被分离开了,容易造成它们的不同步。

One solution to this is an approach like MVC, where you push data changes to the model via the controller, and then the model pushes the new state to the view via the controller. However, this also is problematic, since creating and updating UI elements are two separate steps that can easily get out of sync.

Flutter 与其他响应式框架类似,采用了显式剥离基础状态和用户界面的方式,来解决这一问题。你可以通过 React 风格的 API,创建 UI 的描述,让框架负责通过配置优雅地创建和更新用户界面。

Flutter, along with other reactive frameworks, takes an alternative approach to this problem, by explicitly decoupling the user interface from its underlying state. With React-style APIs, you only create the UI description, and the framework takes care of using that one configuration to both create and/or update the user interface as appropriate.

在 Flutter 里,widgets(类似于 React 中的组件)是用来配置对象树的不可变类。这些 widgets 会管理单独的布局对象树,接着参与管理合成的布局对象树。 Flutter 的核心就是一套高效的遍历树的变动的机制,它会将对象树转换为更底层的对象树,并在树与树之间传递更改。

In Flutter, widgets (akin to components in React) are represented by immutable classes that are used to configure a tree of objects. These widgets are used to manage a separate tree of objects for layout, which is then used to manage a separate tree of objects for compositing. Flutter is, at its core, a series of mechanisms for efficiently walking the modified parts of trees, converting trees of objects into lower-level trees of objects, and propagating changes across these trees.

build() 是将状态转化为 UI 的方法,widget 通过重写该方法来声明 UI 的构造:

A widget declares its user interface by overriding the build() method, which is a function that converts state to UI:

UI = f(state)

build() 方法在框架需要时都可以被调用(每个渲染帧可能会调用一次),从设计角度来看,它应当能够快速执行且没有额外影响的。

The build() method is by design fast to execute and should be free of side effects, allowing it to be called by the framework whenever needed (potentially as often as once per rendered frame).

这样的实现设计依赖于语言的运行时特征(特别是对象的快速实例化和清除)。幸运的是,Dart 非常适合这份工作

This approach relies on certain characteristics of a language runtime (in particular, fast object instantiation and deletion). Fortunately, Dart is particularly well suited for this task.

Widgets

如前所述,Flutter 强调以 widgets 作为组成单位。 Widgets 是构建 Flutter 应用界面的基础块,每个 widget 都是一部分不可变的 UI 声明。

As mentioned, Flutter emphasizes widgets as a unit of composition. Widgets are the building blocks of a Flutter app’s user interface, and each widget is an immutable declaration of part of the user interface.

Widgets 通过布局组合形成一种层次结构关系。每个 Widget 都嵌套在其父级的内部,并可以通过父级接收上下文。从根布局(托管 Flutter 应用的容器,通常是 MaterialAppCupertinoApp)开始,自上而下都是这样的结构,如下面的示例所示:

Widgets form a hierarchy based on composition. Each widget nests inside its parent and can receive context from the parent. This structure carries all the way up to the root widget (the container that hosts the Flutter app, typically MaterialApp or CupertinoApp), as this trivial example shows:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Home Page'),
        ),
        body: Center(
          child: Builder(
            builder: (BuildContext context) {
              return Column(
                children: [
                  const Text('Hello World'),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      print('Click!');
                    },
                    child: const Text('A button'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

在上面的代码中,所有实例化的类都是 widgets。

In the preceding code, all instantiated classes are widgets.

应用会根据事件交互(例如用户操作),通知框架替换层级中的旧 widget 为新 widget,然后框架会比较新旧 widgets,高效地更新用户界面。

Apps update their user interface in response to events (such as a user interaction) by telling the framework to replace a widget in the hierarchy with another widget. The framework then compares the new and old widgets, and efficiently updates the user interface.

Flutter 拥有其自己的 UI 控制实现,而不是由系统自带的方法进行托管:例如, iOS 的 Switch 控件Android 的选择控件 均有一个纯 Dart 实现

Flutter has its own implementations of each UI control, rather than deferring to those provided by the system: for example, there is a pure Dart implementation of both the iOS Switch control and the one for the Android equivalent.

这样的实现有几个优势:

This approach provides several benefits:

组成

Composition

Widgets 通常由更小的且用途单一的 widgets 组合而成,提供更强大的功能。

Widgets are typically composed of many other small, single-purpose widgets that combine to produce powerful effects.

在设计时,相关的设计概念已尽可能地少量存在,而通过大量的内容进行填充。(译者注:即以最小的原语加最多的单一实现创造出最大的价值。)举个例子,Flutter 在 widgets 层中使用了相同的概念(一个 Widget)来表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画及导航。在动画层,AnimationTween 这对概念组合,涵盖了大部分的设计空间。在渲染层,RenderObject 用来描述布局、绘制、触摸判断及可访问性。在这些场景中,最终对应包含的内容都很多:有数百个 widgets 和 render objects,以及数十种动画和补间类型。

Where possible, the number of design concepts is kept to a minimum while allowing the total vocabulary to be large. For example, in the widgets layer, Flutter uses the same core concept (a Widget) to represent drawing to the screen, layout (positioning and sizing), user interactivity, state management, theming, animations, and navigation. In the animation layer, a pair of concepts, Animations and Tweens, cover most of the design space. In the rendering layer, RenderObjects are used to describe layout, painting, hit testing, and accessibility. In each of these cases, the corresponding vocabulary ends up being large: there are hundreds of widgets and render objects, and dozens of animation and tween types.

类的层次结构是有意的浅而广,以最大限度地增加可能的组合数量,重点放在小的、可组合的 widget 上,确保每个 widget 都能很好地完成一件事情。核心功能均被抽象,甚至像边距和对齐这样的基础功能,都被实现为单独的组件,而不是内置于核心中。(这样的实现也与传统的 API 形成了对比,类似边距这样的功能通常都内置在了每个组件的公共核心内, Flutter 中的 widget 则不同。)因此,如果你需要将一个 widget 居中,与其调整 Align 这样的属性,不如将它包裹在一个 Center widget 内。

The class hierarchy is deliberately shallow and broad to maximize the possible number of combinations, focusing on small, composable widgets that each do one thing well. Core features are abstract, with even basic features like padding and alignment being implemented as separate components rather than being built into the core. (This also contrasts with more traditional APIs where features like padding are built in to the common core of every layout component.) So, for example, to center a widget, rather than adjusting a notional Align property, you wrap it in a Center widget.

Flutter 中包含了边距、对齐、行、列和网格系列的 widgets。这些布局类型的 widgets 自身没有视觉内容,而只用于控制其他 widgets 的部分布局条件。 Flutter 也包含了以这种组合方法组成的实用型 widgets。

There are widgets for padding, alignment, rows, columns, and grids. These layout widgets do not have a visual representation of their own. Instead, their sole purpose is to control some aspect of another widget’s layout. Flutter also includes utility widgets that take advantage of this compositional approach.

例如,一个常用的 widget Container,是由几个 widget 组合而成,包含了布局、绘制、定位和大小的功能。更具体地说,Container 是由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 组成的,你也可以通过查看源码看到这些组合。 Flutter 有一个典型的特征,即你可以深入到任意一个 widget,查看其源码。因此,你可以通过同样的方式组合其他的 widgets,也可以参考 Container 来创建其他的 widget,而不需要继承 Container 来实现自定义的效果。

For example, Container, a commonly used widget, is made up of several widgets responsible for layout, painting, positioning, and sizing. Specifically, Container is made up of the LimitedBox, ConstrainedBox, Align, Padding, DecoratedBox, and Transform widgets, as you can see by reading its source code. A defining characteristic of Flutter is that you can drill down into the source for any widget and examine it. So, rather than subclassing Container to produce a customized effect, you can compose it and other simple widgets in novel ways, or just create a new widget using Container as inspiration.

构建 widgets

Building widgets

先前提到,你可以通过重写 build() 方法,返回一个新的元素树,来定义视觉展示。这棵树用更为具体的术语表示了 widget 在 UI 中的部分。例如,工具栏 widget 的 build 方法可能会返回 水平布局,其中可能包含一些 文字各种各样按钮。根据需要,框架会递归请求每个 widget 进行构建,直到整棵树都被 具体的可渲染对象 描述为止。然后,框架会将可渲染的对象缝合在一起,组成可渲染对象树。

As mentioned earlier, you determine the visual representation of a widget by overriding the build() function to return a new element tree. This tree represents the widget’s part of the user interface in more concrete terms. For example, a toolbar widget might have a build function that returns a horizontal layout of some text and various buttons. As needed, the framework recursively asks each widget to build until the tree is entirely described by concrete renderable objects. The framework then stitches together the renderable objects into a renderable object tree.

Widget 的 build 方法应该是没有副作用的。每当一个方法要求构建时, widget 都应当能返回一个 widget 的元素树1,与先前返回的 widget 也没有关联。框架会根据渲染对象树(稍后将进一步介绍)来确定哪些构建方法需要被调用,这是一项略显繁重的工作。有关这个过程的更多信息,可以在 Flutter 工作原理 中进一步了解。

A widget’s build function should be free of side effects. Whenever the function is asked to build, the widget should return a new tree of widgets1, regardless of what the widget previously returned. The framework does the heavy lifting work to determine which build methods need to be called based on the render object tree (described in more detail later). More information about this process can be found in the Inside Flutter topic.

每个渲染帧,Flutter 都可以根据变化的状态,调用 build() 方法重建部分 UI。因此,保证 build 方法轻量且能快速返回 widget 是非常关键的,繁重的计算工作应该通过一些异步方法完成,并存储在状态中,在 build 方法中使用。

On each rendered frame, Flutter can recreate just the parts of the UI where the state has changed by calling that widget’s build() method. Therefore it is important that build methods should return quickly, and heavy computational work should be done in some asynchronous manner and then stored as part of the state to be used by a build method.

尽管这样的实现看起来不够成熟,但这样的自动对比方法非常有效,可以实现高性能的交互应用。同时,以这种方式设计的 build 方法,将着重点放在 widget 组成的声明上,从而简化了你的代码,而不是以一种状态去更新另一种状态这样的复杂过程。

While relatively naïve in approach, this automated comparison is quite effective, enabling high-performance, interactive apps. And, the design of the build function simplifies your code by focusing on declaring what a widget is made of, rather than the complexities of updating the user interface from one state to another.

Widget 的状态

Widget state

框架包含两种核心的 widget 类:有状态的无状态的 widget。

The framework introduces two major classes of widget: stateful and stateless widgets.

大部分 widget 都没有需要变更的状态:它们并不包含随时变化的属性(例如图标或者标签)。这些 widget 会继承 StatelessWidget

Many widgets have no mutable state: they don’t have any properties that change over time (for example, an icon or a label). These widgets subclass StatelessWidget.

然而,当 widget 拥有需要根据用户交互或其他因素而变化的特有属性,它就是 有状态的。例如,计数器 widget 在用户点击按钮时数字递增,那么计数值就是计数器 widget 的状态。当值变化时,widget 则需要被重建以更新相关部分的 UI。这些 widget 会继承 StatefulWidget,并且「可变的」状态会保存在继承 State 的另一个子类中(因为 widget 本身是不可变的)。 StatefulWidget 自身没有 build 方法,而在其对应的 State 对象中。

However, if the unique characteristics of a widget need to change based on user interaction or other factors, that widget is stateful. For example, if a widget has a counter that increments whenever the user taps a button, then the value of the counter is the state for that widget. When that value changes, the widget needs to be rebuilt to update its part of the UI. These widgets subclass StatefulWidget, and (because the widget itself is immutable) they store mutable state in a separate class that subclasses State. StatefulWidgets don’t have a build method; instead, their user interface is built through their State object.

每当你更改 State 对象时(例如计数增加),你需要调用 setState() 来告知框架,再次调用 State 的构建方法来更新 UI。

Whenever you mutate a State object (for example, by incrementing the counter), you must call setState() to signal the framework to update the user interface by calling the State’s build method again.

将状态和 widget 对象分离,可以使其他 widget 无差异地看待无状态和有状态 widget,而不必担心丢失状态。父级无需担心状态的丢失,可以随时创建新的实例,并不需要通过子级关系保持其状态。框架也会在合适的时间,复用已存在的状态对象。

Having separate state and widget objects lets other widgets treat both stateless and stateful widgets in exactly the same way, without being concerned about losing state. Instead of needing to hold on to a child to preserve its state, the parent can create a new instance of the child at any time without losing the child’s persistent state. The framework does all the work of finding and reusing existing state objects when appropriate.

状态管理

State management Available

那么,在众多 widget 都持有状态的情况下,系统中的状态是如何被传递和管理的呢?

So, if many widgets can contain state, how is state managed and passed around the system?

与其他类相同,你可以通过 widget 的构造函数来初始化数据,如此一来 build() 方法可以确保子 widget 使用其所需的数据进行实例化:

As with any other class, you can use a constructor in a widget to initialize its data, so a build() method can ensure that any child widget is instantiated with the data it needs:

@override
Widget build(BuildContext context) {
   return ContentWidget(importantState);
}

然而,随着 widget 树层级逐渐加深,依赖树形结构上下传递状态信息会变得十分麻烦。这时,第三种类型的 widget—— InheritedWidget,提供了一种从共同的祖先节点获取数据的简易方法。你可以使用 InheritedWidget 创建包含状态的 widget,该 widget 会将一个共同的祖先节点包裹在 widget 树中,如下面的例子所示:

As widget trees get deeper, however, passing state information up and down the tree hierarchy becomes cumbersome. So, a third widget type, InheritedWidget, provides an easy way to grab data from a shared ancestor. You can use InheritedWidget to create a state widget that wraps a common ancestor in the widget tree, as shown in this example:

Inherited widgets

现在,当 ExamWidgetGradeWidget 对象需要获取 StudentState 的数据时,可以直接使用以下方式:

Whenever one of the ExamWidget or GradeWidget objects needs data from StudentState, it can now access it with a command such as:

final studentState = StudentState.of(context);

调用 of(context) 会根据当前构建的上下文(即当前 widget 位置的句柄),并返回类型为 StudentState在树中距离最近的祖先节点InheritedWidget 同时也包含了 updateShouldNotify() 方法, Flutter 会调用它来判断依赖了某个状态的 widget 是否需要重建。

The of(context) call takes the build context (a handle to the current widget location), and returns the nearest ancestor in the tree that matches the StudentState type. InheritedWidgets also offer an updateShouldNotify() method, which Flutter calls to determine whether a state change should trigger a rebuild of child widgets that use it.

InheritedWidget 在 Flutter 框架中被大量用于共享状态,例如应用的 视觉主题,包含了应用于整个应用的 颜色和字体样式等属性MaterialAppbuild() 方法会在构建时在树中插入一个主题,更深层级的 widget 便可以使用 .of() 方法来查找相关的主题数据,例如:

Flutter itself uses InheritedWidget extensively as part of the framework for shared state, such as the application’s visual theme, which includes properties like color and type styles that are pervasive throughout an application. The MaterialApp build() method inserts a theme in the tree when it builds, and then deeper in the hierarchy a widget can use the .of() method to look up the relevant theme data, for example:

Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.headline6,
  ),
);

类似地,以该方法实现的还有提供了页面路由的 Navigator、提供了屏幕信息指标,包括方向、尺寸和亮度的 MediaQuery 等。

This approach is also used for Navigator, which provides page routing; and MediaQuery, which provides access to screen metrics such as orientation, dimensions, and brightness.

随着应用程序的不断迭代,更高级的状态管理方法变得更有吸引力,它们可以减少有状态的 widget 的创建。许多 Flutter 应用使用了 provider 用于状态管理,它对 InheritedWidget 进行了进一步的包装。 Flutter 的分层架构也允许使用其他实现来替换状态至 UI 的方案,例如 flutter_hooks

As applications grow, more advanced state management approaches that reduce the ceremony of creating and using stateful widgets become more attractive. Many Flutter apps use utility packages like provider, which provides a wrapper around InheritedWidget. Flutter’s layered architecture also enables alternative approaches to implement the transformation of state into UI, such as the flutter_hooks package.

渲染和布局

Rendering and layout

本节介绍 Flutter 的渲染机制,包括将 widget 层级结构转换成屏幕上绘制的实际像素的一系列步骤。

This section describes the rendering pipeline, which is the series of steps that Flutter takes to convert a hierarchy of widgets into the actual pixels painted onto a screen.

Flutter 的渲染模型

Flutter’s rendering model

你可能思考过:既然 Flutter 是一个跨平台的框架,那么它如何提供与原生平台框架相当的性能?

You may be wondering: if Flutter is a cross-platform framework, then how can it offer comparable performance to single-platform frameworks?

让我们从安卓原生应用的角度开始思考。当你在编写绘制的内容时,你需要调用 Android 框架的 Java 代码。 Android 的系统库提供了可以将自身绘制到 Canvas 对象的组件,接下来 Android 就可以使用由 C/C++ 编写的 Skia 图像引擎,调用 CPU 和 GPU 完成在设备上的绘制。

It’s useful to start by thinking about how traditional Android apps work. When drawing, you first call the Java code of the Android framework. The Android system libraries provide the components responsible for drawing themselves to a Canvas object, which Android can then render with Skia, a graphics engine written in C/C++ that calls the CPU or GPU to complete the drawing on the device.

通常来说,跨平台框架都会在 Android 和 iOS 的 UI 底层库上创建一层抽象,该抽象层尝试抹平各个系统之间的差异。这时,应用程序的代码常常使用 JavaScript 等解释型语言来进行编写,这些代码会与基于 Java 的 Android 和基于 Objective-C 的 iOS 系统进行交互,最终显示 UI 界面。所有的流程都增加了显著的开销,在 UI 和应用逻辑有繁杂的交互时更为如此。

Cross-platform frameworks typically work by creating an abstraction layer over the underlying native Android and iOS UI libraries, attempting to smooth out the inconsistencies of each platform representation. App code is often written in an interpreted language like JavaScript, which must in turn interact with the Java-based Android or Objective-C-based iOS system libraries to display UI. All this adds overhead that can be significant, particularly where there is a lot of interaction between the UI and the app logic.

相比之下,Flutter 通过绕过系统 UI 组件库,使用自己的 widget 内容集,削减了抽象层的开销。用于绘制 Flutter 图像内容的 Dart 代码被编译为机器码,并使用 Skia 进行渲染。 Flutter 同时也嵌入了自己的 Skia 副本,让开发者能在设备未更新到最新的系统时,也能跟进升级自己的应用,保证稳定性并提升性能。

By contrast, Flutter minimizes those abstractions, bypassing the system UI widget libraries in favor of its own widget set. The Dart code that paints Flutter’s visuals is compiled into native code, which uses Skia for rendering. Flutter also embeds its own copy of Skia as part of the engine, allowing the developer to upgrade their app to stay updated with the latest performance improvements even if the phone hasn’t been updated with a new Android version. The same is true for Flutter on other native platforms, such as iOS, Windows, or macOS.

从用户操作到 GPU

From user input to the GPU

对于 Flutter 的渲染机制而言,首要原则是 简单快速。 Flutter 为数据流向系统提供了直通的管道,如以下的流程图所示:

The overriding principle that Flutter applies to its rendering pipeline is that simple is fast. Flutter has a straightforward pipeline for how data flows to the system, as shown in the following sequencing diagram:

Render pipeline sequencing
diagram

接下来,让我们更加深入了解其中的一些阶段。

Let’s take a look at some of these phases in greater detail.

构建:从 Widget 到 Element

Build: from Widget to Element

首先观察以下的代码片段,它代表了一个简单的 widget 结构:

Consider this simple code fragment that demonstrates a simple widget hierarchy:

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当 Flutter 需要绘制这段代码片段时,框架会调用 build() 方法,返回一棵基于当前应用状态来绘制 UI 的 widget 子树。在这个过程中,build() 方法可能会在必要时,根据状态引入新的 widget。在上面的例子中,Containercolorchild 就是典型的例子。我们可以查看 Container源代码,你会看到当 color 属性不为空时,ColoredBox 会被加入用于颜色布局。

When Flutter needs to render this fragment, it calls the build() method, which returns a subtree of widgets that renders UI based on the current app state. During this process, the build() method can introduce new widgets, as necessary, based on its state. As a simple example, in the preceding code fragment, Container has color and child properties. From looking at the source code for Container, you can see that if the color is not null, it inserts a ColoredBox representing the color:

if (color != null)
  current = ColoredBox(color: color!, child: current);

与之对应的,ImageText 在构建过程中也会引入 RawImageRichText。如此一来,最终生成的 widget 结构比代码表示的层级更深,在该场景中如下图2

Correspondingly, the Image and Text widgets might insert child widgets such as RawImage and RichText during the build process. The eventual widget hierarchy may therefore be deeper than what the code represents, as in this case2:

Render pipeline sequencing
diagram

这就是为什么你在使用 Dart DevTools 的 Flutter inspector 调试 widget 树结构时,会发现实际的结构比你原本代码中的结构层级更深。

This explains why, when you examine the tree through a debug tool such as the Flutter inspector, part of the Dart DevTools, you might see a structure that is considerably deeper than what is in your original code.

在构建的阶段,Flutter 会将代码中描述的 widgets 转换成对应的 Element 树,每一个 Widget 都有一个对应的 Element。每一个 Element 代表了树状层级结构中特定位置的 widget 实例。目前有两种 Element 的基本类型:

During the build phase, Flutter translates the widgets expressed in code into a corresponding element tree, with one element for every widget. Each element represents a specific instance of a widget in a given location of the tree hierarchy. There are two basic types of elements:

Render pipeline sequencing
diagram

RenderObjectElement 是底层 RenderObject 与对应的 widget 之间的桥梁,我们晚些会介绍它。

RenderObjectElements are an intermediary between their widget analog and the underlying RenderObject, which we’ll come to later.

任何 widget 都可以通过其 BuildContext 引用到 Element,它是该 widget 在树中的位置的句柄。类似 Theme.of(context) 方法调用中的 context,它作为 build() 方法的参数被传递。

The element for any widget can be referenced through its BuildContext, which is a handle to the location of a widget in the tree. This is the context in a function call such as Theme.of(context), and is supplied to the build() method as a parameter.

由于 widgets 以及它上下节点的关系都是不可变的,因此,对 widget 树做的任何操作(例如将 Text('A') 替换成 Text('B'))都会返回一个新的 widget 对象集合。但这并不意味着底层呈现的内容必须要重新构建。 Element 树每一帧之间都是持久化的,因此起着至关重要的性能作用, Flutter 依靠该优势,实现了一种好似 widget 树被完全抛弃,而缓存了底层表示的机制。 Flutter 可以根据发生变化的 widget,来重建需要重新配置的 Element 树的部分。

Because widgets are immutable, including the parent/child relationship between nodes, any change to the widget tree (such as changing Text('A') to Text('B') in the preceding example) causes a new set of widget objects to be returned. But that doesn’t mean the underlying representation must be rebuilt. The element tree is persistent from frame to frame, and therefore plays a critical performance role, allowing Flutter to act as if the widget hierarchy is fully disposable while caching its underlying representation. By only walking through the widgets that changed, Flutter can rebuild just the parts of the element tree that require reconfiguration.

布局和渲染

Layout and rendering

很少有应用只绘制单个 widget。因此,有效地排布 widget 的结构及在渲染完成前决定每个 Element 的大小和位置,是所有 UI 框架的重点之一。

It would be a rare application that drew only a single widget. An important part of any UI framework is therefore the ability to efficiently lay out a hierarchy of widgets, determining the size and position of each element before they are rendered on the screen.

在渲染树中,每个节点的基类都是 RenderObject,该基类为布局和绘制定义了一个抽象模型。这是再平凡不过的事情:它并不总是一个固定的大小,甚至不遵循笛卡尔坐标规律(根据该 极坐标系的示例 所示)。每一个 RenderObject 都了解其父节点的信息,但对于其子节点,除了如何 访问 和获得他们的布局约束,并没有更多的信息。这样的设计让 RenderObject 拥有高效的抽象能力,能够处理各种各样的使用场景。

The base class for every node in the render tree is RenderObject, which defines an abstract model for layout and painting. This is extremely general: it does not commit to a fixed number of dimensions or even a Cartesian coordinate system (demonstrated by this example of a polar coordinate system). Each RenderObject knows its parent, but knows little about its children other than how to visit them and their constraints. This provides RenderObject with sufficient abstraction to be able to handle a variety of use cases.

在构建阶段,Flutter 会为 Element 树中的每个 RenderObjectElement 创建或更新其对应的一个从 RenderObject 继承的对象。 RenderObject 实际上是原语:渲染文字的 RenderParagraph、渲染图片的 RenderImage 以及在绘制子节点内容前应用变换的 RenderTransform 是更为上层的实现。

During the build phase, Flutter creates or updates an object that inherits from RenderObject for each RenderObjectElement in the element tree. RenderObjects are primitives: RenderParagraph renders text, RenderImage renders an image, and RenderTransform applies a transformation before painting its child.

Differences between the widgets hierarchy and the element and render
trees

大部分的 Flutter widget 是由一个继承了 RenderBox 的子类的对象渲染的,它们呈现出的 RenderObject 会在二维笛卡尔空间中拥有固定的大小。 RenderBox 提供了 盒子限制模型,为每个 widget 关联了渲染的最小和最大的宽度和高度。

Most Flutter widgets are rendered by an object that inherits from the RenderBox subclass, which represents a RenderObject of fixed size in a 2D Cartesian space. RenderBox provides the basis of a box constraint model, establishing a minimum and maximum width and height for each widget to be rendered.

在进行布局的时候,Flutter 会以 DFS(深度优先遍历)方式遍历渲染树,并 将限制以自上而下的方式 从父节点传递给子节点。子节点若要确定自己的大小,则 必须 遵循父节点传递的限制。子节点的响应方式是在父节点建立的约束内 将大小以自下而上的方式 传递给父节点。

To perform layout, Flutter walks the render tree in a depth-first traversal and passes down size constraints from parent to child. In determining its size, the child must respect the constraints given to it by its parent. Children respond by passing up a size to their parent object within the constraints the parent established.

Constraints go down, sizes go
up

在遍历完一次树后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用 paint() 进行渲染。

At the end of this single walk through the tree, every object has a defined size within its parent’s constraints and is ready to be painted by calling the paint() method.

盒子限制模型十分强大,它的对象布局的时间复杂度是 O(n)

The box constraint model is very powerful as a way to layout objects in O(n) time:

这样的盒子约束模型,同样也适用于子节点对象需要知道有多少可用空间渲染其内容的场景,通过使用 LayoutBuilder widget,子节点可以得到从上层传递下来的约束,并合理利用该约束对象,使用方法如下:

This model works even when a child object needs to know how much space it has available to decide how it will render its content. By using a LayoutBuilder widget, the child object can examine the passed-down constraints and use those to determine how it will use them, for example:

Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

更多有关约束和布局系统的信息,及可参考的例子,可以在 深入理解 Flutter 布局约束 文章中查看。

More information about the constraint and layout system, along with worked examples, can be found in the Understanding constraints topic.

所有 RenderObject 的根节点是 RenderView,代表了渲染树的总体输出。当平台需要渲染新的一帧内容时(例如一个 vsync 信号或者一个纹理的更新完成),会调用一次 compositeFrame() 方法,它是 RenderView 的一部分。该方法会创建一个 SceneBuilder 来触发当前画面的更新。当画面更新完毕,RenderView 会将合成的画面传递给 dart:ui 中的 Window.render() 方法,控制 GPU 进行渲染。

The root of all RenderObjects is the RenderView, which represents the total output of the render tree. When the platform demands a new frame to be rendered (for example, because of a vsync or because a texture decompression/upload is complete), a call is made to the compositeFrame() method, which is part of the RenderView object at the root of the render tree. This creates a SceneBuilder to trigger an update of the scene. When the scene is complete, the RenderView object passes the composited scene to the Window.render() method in dart:ui, which passes control to the GPU to render it.

有关渲染流程的合成和栅格化阶段的更多细节,将不在本篇深入文章中讨论,但可以在 关于 Flutter 渲染流程的讨论 中了解更多。

Further details of the composition and rasterization stages of the pipeline are beyond the scope of this high-level article, but more information can be found in this talk on the Flutter rendering pipeline.

Platform embedding

我们都知道,Flutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件。获取纹理和联动应用底层的生命周期的方法,不可避免地会根据平台特性而改变。 Flutter 引擎本身是与平台无关的,它提供了一个稳定的 ABI(应用二进制接口),包含一个 平台嵌入层,可以通过其方法设置并使用 Flutter。

As we’ve seen, rather than being translated into the equivalent OS widgets, Flutter user interfaces are built, laid out, composited, and painted by Flutter itself. The mechanism for obtaining the texture and participating in the app lifecycle of the underlying operating system inevitably varies depending on the unique concerns of that platform. The engine is platform-agnostic, presenting a stable ABI (Application Binary Interface) that provides a platform embedder with a way to set up and use Flutter.

平台嵌入层是用于呈现所有 Flutter 内容的原生系统应用,它充当着宿主操作系统和 Flutter 之间的粘合剂的角色。当你启动一个 Flutter 应用时,嵌入层会提供一个入口,初始化 Flutter 引擎,获取 UI 和栅格化线程,创建 Flutter 可以写入的纹理。嵌入层同时负责管理应用的生命周期,包括输入的操作(例如鼠标、键盘和触控)、窗口大小的变化、线程管理和平台消息的传递。 Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层,当然,开发者可以创建自定义的嵌入层,正如这个 可用的例子 以 VNC 风格的帧缓冲区支持了远程 Flutter,还有 [支持树莓派运行的例子]https://github.com/ardera/flutter-pi)。

The platform embedder is the native OS application that hosts all Flutter content, and acts as the glue between the host operating system and Flutter. When you start a Flutter app, the embedder provides the entrypoint, initializes the Flutter engine, obtains threads for UI and rastering, and creates a texture that Flutter can write to. The embedder is also responsible for the app lifecycle, including input gestures (such as mouse, keyboard, touch), window sizing, thread management, and platform messages. Flutter includes platform embedders for Android, iOS, Windows, macOS, and Linux; you can also create a custom platform embedder, as in this worked example that supports remoting Flutter sessions through a VNC-style framebuffer or this worked example for Raspberry Pi.

每一个平台都有各自的一套 API 和限制。以下是一些关于平台简短的说明:

Each platform has its own set of APIs and constraints. Some brief platform-specific notes:

与其他代码进行集成

Integrating with other code

Flutter 提供了多种代码交互机制,无论你是在调用 Kotlin 或者 Swift 这些语言编写的代码或 API,或是调用 C 语言基础的 API,或是将原生代码能力嵌入 Flutter 应用,又或是将 Flutter 嵌入现有的应用。

Flutter provides a variety of interoperability mechanisms, whether you’re accessing code or APIs written in a language like Kotlin or Swift, calling a native C-based API, embedding native controls in a Flutter app, or embedding Flutter in an existing application.

平台通道

Platform channels

对于移动端和桌面端应用而言,Flutter 提供了通过 平台通道 调用自定义代码的能力,这是一种非常简单的在宿主应用之间让 Dart 代码与平台代码通信的机制。通过创建一个常用的通道(封装通道名称和编码),开发者可以在 Dart 与使用 Kotlin 和 Swift 等语言编写的平台组件之间发送和接收消息。数据会由 Dart 类型(例如 Map)序列化为一种标准格式,然后反序列化为 Kotlin(例如 HashMap)或者 Swift(例如 Dictionary)中的等效类型。

For mobile and desktop apps, Flutter allows you to call into custom code through a platform channel, which is a simple mechanism for communicating between your Dart code and the platform-specific code of your host app. By creating a common channel (encapsulating a name and a codec), you can send and receive messages between Dart and a platform component written in a language like Kotlin or Swift. Data is serialized from a Dart type like Map into a standard format, and then deserialized into an equivalent representation in Kotlin (such as HashMap) or Swift (such as Dictionary).

How platform channels allow Flutter to communicate with host
code

下方的示例是在 Kotlin (Android) 或 Swift (iOS) 中处理 Dart 调用平台通道事件的简单接收处理:

The following is a simple platform channel example of a Dart call to a receiving event handler in Kotlin (Android) or Swift (iOS):

// Dart side
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    else -> result.notImplemented()
  }
}
// iOS (Swift)
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
  (call: FlutterMethodCall, result: FlutterResult) -> Void in
  switch (call.method) {
    case "bar": result("Hello, \(call.arguments as! String)")
    default: result(FlutterMethodNotImplemented)
  }
}

更多关于如何使用平台通道的例子,包括 macOS 平台的示例,可以在 flutter/plugins 代码仓库 3找到。

Further examples of using platform channels, including examples for macOS, can be found in the flutter/plugins repository3. There are also thousands of plugins already available for Flutter that cover many common scenarios, ranging from Firebase to ads to device hardware like camera and Bluetooth.

外部函数接口

Foreign Function Interface

对于基于 C 语言的 API,包括使用现代语言 Rust 或 Go 生成的代码, Dart 也提供了 dart:ffi 库,一套直接绑定原生代码的机制。外部函数接口 (foreign function interface,FFI) 比平台通道更快,因为不需要序列化即可传递数据。实际上,Dart 的运行时提供了在堆上分配 Dart 对象内存的支持,以及调用静态或动态链接库的能力。除了 Web 平台外,FFI 在其他平台均可以使用,因为 Web 平台上的 js 包 已经具有相同的用途。

For C-based APIs, including those that can be generated for code written in modern languages like Rust or Go, Dart provides a direct mechanism for binding to native code using the dart:ffi library. The foreign function interface (FFI) model can be considerably faster than platform channels, because no serialization is required to pass data. Instead, the Dart runtime provides the ability to allocate memory on the heap that is backed by a Dart object and make calls to statically or dynamically linked libraries. FFI is available for all platforms other than web, where the js package serves an equivalent purpose.

若您需要使用 FFI,请为每一个 Dart 和未经管理的函数的签名创建一个 typedef,并且指示 Dart VM 为它们创建关联。下面这段代码片段是调用 Win32 的 MessageBox() API 的简单示例:

To use FFI, you create a typedef for each of the Dart and unmanaged method signatures, and instruct the Dart VM to map between them. As a simple example, here’s a fragment of code to call the traditional Win32 MessageBox() API:

typedef MessageBoxNative = Int32 Function(
  IntPtr hWnd,
  Pointer<Utf16> lpText,
  Pointer<Utf16> lpCaption,
  Int32 uType,
);

typedef MessageBoxDart = int Function(
  int hWnd,
  Pointer<Utf16> lpText,
  Pointer<Utf16> lpCaption,
  int uType,
);

void exampleFfi() {
  final user32 = DynamicLibrary.open('user32.dll');
  final messageBox =
      user32.lookupFunction<MessageBoxNative, MessageBoxDart>('MessageBoxW');

  final result = messageBox(
    0, // No owner window
    'Test message'.toNativeUtf16(), // Message
    'Window caption'.toNativeUtf16(), // Window title
    0, // OK button only
  );
}

在 Flutter 应用中渲染原生内容

Rendering native controls in a Flutter app

由于 Flutter 的内容会绘制在单一的纹理内,并且 widget 树是完全在内部的,因此在 Flutter 的内部模型中无法存在 Android 视图之类的内容,也无法与 Flutter 的 widget 交错渲染对于需要在 Flutter 应用中展示原生组件(例如内置浏览器)的开发者来说,这是一个问题。

Because Flutter content is drawn to a texture and its widget tree is entirely internal, there’s no place for something like an Android view to exist within Flutter’s internal model or render interleaved within Flutter widgets. That’s a problem for developers that would like to include existing platform components in their Flutter apps, such as a browser control.

Flutter 通过引入了平台 widget (AndroidViewUiKitView) 解决了这个问题,开发者可以在每一种平台上嵌入此类内容。平台视图可以与其他的 Flutter 内容集成4。这些 widget 充当了底层操作系统与 Flutter 之间的桥梁。例如在 Android 上,AndroidView 主要提供了三项功能:

Flutter solves this by introducing platform view widgets (AndroidView and UiKitView) that let you embed this kind of content on each platform. Platform views can be integrated with other Flutter content4. Each of these widgets acts as an intermediary to the underlying operating system. For example, on Android, AndroidView serves three primary functions:

但不可避免的是,这样的同步操作必然会带来相应的开销。因此该方法通常更适合复杂的控件,例如谷歌地图这种不适合在 Flutter 中重新实现的。

Inevitably, there is a certain amount of overhead associated with this synchronization. In general, therefore, this approach is best suited for complex controls like Google Maps where reimplementing in Flutter isn’t practical.

通常 Flutter 应用会在 build() 方法中基于平台判断来实例化这些 widget。例如在 google_maps_flutter 插件中:

Typically, a Flutter app instantiates these widgets in a build() method based on a platform test. As an example, from the google_maps_flutter plugin:

if (defaultTargetPlatform == TargetPlatform.android) {
  return AndroidView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
  return UiKitView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}
return Text(
    '$defaultTargetPlatform is not yet supported by the maps plugin');

如上文所述,AndroidViewUiKitView 通常是利用平台通道的机制与原生进行通信。

Communicating with the native code underlying the AndroidView or UiKitView typically occurs using the platform channels mechanism, as previously described.

目前桌面平台尚未支持平台视图,但这并不是一个架构层面的限制。未来可能将增加对桌面平台的支持。

At present, platform views aren’t available for desktop platforms, but this is not an architectural limitation; support might be added in the future.

在上层应用中托管 Flutter 内容

Hosting Flutter content in a parent app

与上一个场景相反的是,将 Flutter widget 集成至现有的 Android 或 iOS 应用中。先前提到,新创建的 Flutter 应用,在移动设备上是在一个 Android 的 Activity 或 iOS 的 UIViewController 中运行。开发者可以使用相同的嵌入 API 将 Flutter 内容集成至现有的 Android 或 iOS 应用中。

The converse of the preceding scenario is embedding a Flutter widget in an existing Android or iOS app. As described in an earlier section, a newly created Flutter app running on a mobile device is hosted in an Android activity or iOS UIViewController. Flutter content can be embedded into an existing Android or iOS app using the same embedding API.

Flutter 模块模板设计简单,易于嵌入。开发者可以将其作为源代码依赖项集成到 Gradle 或 Xcode 构建定义中,或者将其打包成 Android Archive (AAR) 或 iOS Framework 二进制供其他开发者使用,而无需安装 Flutter。

The Flutter module template is designed for easy embedding; you can either embed it as a source dependency into an existing Gradle or Xcode build definition, or you can compile it into an Android Archive or iOS Framework binary for use without requiring every developer to have Flutter installed.

Flutter 引擎需要一段短暂的时间做初始化,用于加载 Flutter 的共享库、初始化 Dart 的运行时、创建并运行 Dart isolate 线程并将渲染层与 UI 进行绑定。为了最大限度地减少呈现 Flutter 界面时的延迟,最好是在应用初始化时或至少在第一个 Flutter 页面展示前,一并初始化 Flutter 引擎,如此一来用户不会在首个 Flutter 页面加载时感到突然地卡顿。另外,Flutter 的引擎分离使得多个 Flutter 页面可以复用引擎,共享必要库加载时的内存消耗。

The Flutter engine takes a short while to initialize, because it needs to load Flutter shared libraries, initialize the Dart runtime, create and run a Dart isolate, and attach a rendering surface to the UI. To minimize any UI delays when presenting Flutter content, it’s best to initialize the Flutter engine during the overall app initialization sequence, or at least ahead of the first Flutter screen, so that users don’t experience a sudden pause while the first Flutter code is loaded. In addition, separating the Flutter engine allows it to be reused across multiple Flutter screens and share the memory overhead involved with loading the necessary libraries.

更多将 Flutter 集成至现有的 Android 和 iOS 应用的内容,可在 控制加载顺序,优化性能与内存 文章中查看。

More information about how Flutter is loaded into an existing Android or iOS app can be found at the Load sequence, performance and memory topic.

Flutter 对 Web 的支持

Flutter web support

虽然 Flutter 支持的所有平台的都适用于同一个架构概念,但是在 Web 平台的支持上有一些独特的特征值得说明。

While the general architectural concepts apply to all platforms that Flutter supports, there are some unique characteristics of Flutter’s web support that are worthy of comment.

Dart 语言存在之初就已经支持直接编译成 JavaScript,并且针对开发和生产目的对其工具链进行了优化。许多重要的应用已经使用 Dart 编译成的 JavaScript 在生产环境上运行,包括 Google Ads 的广告商工具。由于 Flutter 框架是 Dart 编写的,将其编译成 JavaScript 相对而言更为简单。

Dart has been compiling to JavaScript for as long as the language has existed, with a toolchain optimized for both development and production purposes. Many important apps compile from Dart to JavaScript and run in production today, including the advertiser tooling for Google Ads. Because the Flutter framework is written in Dart, compiling it to JavaScript was relatively straightforward.

然而,使用 C++ 编写的 Flutter 引擎是为了与底层操作系统进行交互的,而不是 Web 浏览器。因此我们需要另辟蹊径。Flutter 在 Web 平台上以浏览器的标准 API 重新实现了引擎。目前我们有两种在 Web 上呈现内容的选项:HTML 和 WebGL。在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG 进行渲染。而在 WebGL 模式下,Flutter 使用了一个编译为 WebAssembly 的 Skia 版本,名为 CanvasKit。 HTML 模式提供了最佳的代码大小,CanvasKit 则提供了浏览器图形堆栈渲染的最快途径,并为原生平台的内容5提供了更高的图形保真度。

However, the Flutter engine, written in C++, is designed to interface with the underlying operating system rather than a web browser. A different approach is therefore required. On the web, Flutter provides a reimplementation of the engine on top of standard browser APIs. We currently have two options for rendering Flutter content on the web: HTML and WebGL. In HTML mode, Flutter uses HTML, CSS, Canvas, and SVG. To render to WebGL, Flutter uses a version of Skia compiled to WebAssembly called CanvasKit. While HTML mode offers the best code size characteristics, CanvasKit provides the fastest path to the browser’s graphics stack, and offers somewhat higher graphical fidelity with the native mobile targets5.

Web 版本的分层架构图如下所示:

The web version of the architectural layer diagram is as follows:

Flutter web
architecture

与其他运行 Flutter 的平台相比,最明显的区别也许是 Flutter 不再需要提供 Dart 的运行时。取而代之的是 Flutter 框架本身(和你写的代码)一并编译成 JavaScript。另外值得注意的是,Dart 在不同模式下(JIT 和 AOT、平台原生和 Web 编译)的语义几乎没有差异,大部分开发者绝对可以无差异地编写这两种模式下的代码。

Perhaps the most notable difference compared to other platforms on which Flutter runs is that there is no need for Flutter to provide a Dart runtime. Instead, the Flutter framework (along with any code you write) is compiled to JavaScript. It’s also worthy to note that Dart has very few language semantic differences across all its modes (JIT versus AOT, native versus web compilation), and most developers will never write a line of code that runs into such a difference.

在进行开发时,Web 版本的 Flutter 使用支持增量编译的编译器 dartdevc 进行编译,以支持应用热重启(尽管目前尚未支持热重载)。相反,当你准备好创建一个生产环境的 Web 应用时,Dart 深度优化的编译器 dart2js 将会用于编译,将 Flutter 核心框架和你的应用打包至缩小的源文件中,可部署在任何服务器上。代码可以在单个文件中提供,也可拆分至多个文件以 延迟加载库 提供。

During development time, Flutter web uses dartdevc, a compiler that supports incremental compilation and therefore allows hot restart (although not currently hot reload) for apps. Conversely, when you are ready to create a production app for the web, dart2js, Dart’s highly-optimized production JavaScript compiler is used, packaging the Flutter core and framework along with your application into a minified source file that can be deployed to any web server. Code can be offered in a single file or split into multiple files through deferred imports.

更多信息

Further information

若你对 Flutter 的更多内部细节感兴趣 Flutter 工作原理 白皮书为框架的设计理念提供了很好的入门途径。

For those interested in more information about the internals of Flutter, the Inside Flutter whitepaper provides a useful guide to the framework’s design philosophy.


脚注:

Footnotes:

1build 方法返回一个全新的结构树时,你只需要返回不同的内容,就可以合并一些新的配置。如果配置实际上是相同的,完全可以返回同样的 widget。

1 While the build function returns a fresh tree, you only need to return something different if there’s some new configuration to incorporate. If the configuration is in fact the same, you can just return the same widget.

2 为了便于阅读,该图已进行简化。实际上的结构可能更为复杂。

2 This is a slight simplification for ease of reading. In practice, the tree might be more complex.

3 在 Linux 和 Windows 平台的开发进程中,平台对应的示例可以在 Flutter 桌面集成代码仓库 中找到。随着这些平台的开发愈发成熟,这些内容会逐步迁移到 Flutter 主代码仓库中。

3 While work is underway on Linux and Windows, examples for those platforms can be found in the Flutter desktop embedding repository. As development on those platforms reaches maturity, this content will be gradually migrated into the main Flutter repository.

4 该方法有一些局限性,例如,平台视图的透明度计算与其他 Flutter widget 的计算不同。

4 There are some limitations with this approach, for example, transparency doesn’t composite the same way for a platform view as it would for other Flutter widgets.

5 其中一个例子便是阴影,它必须以等效于 DOM 原语的内容来实现,并且需要丢失一定的保真度。

5 One example is shadows, which have to be approximated with DOM-equivalent primitives at the cost of some fidelity.

Flutter 工作原理

目录

本文档解释了使 Flutter API 正常工作的 Flutter 工具包内部工作原理。由于 Flutter widget 是以积极组合的形式构建的,所以使用 Flutter 构建的用户界面含有大量 widget。为了支撑这些负载,Flutter 使用了次线性算法来布局和构建 widget,这些数据结构使树形结构优化更加高效,并且具有很多常量因子优化。通过一些额外的机制,该设计也允许开发者利用回调(用于构建用户可见的 widget)来轻松创建无限滚动列表。

This document describes the inner workings of the Flutter toolkit that make Flutter’s API possible. Because Flutter widgets are built using aggressive composition, user interfaces built with Flutter have a large number of widgets. To support this workload, Flutter uses sublinear algorithms for layout and building widgets as well as data structures that make tree surgery efficient and that have a number of constant-factor optimizations. With some additional details, this design also makes it easy for developers to create infinite scrolling lists using callbacks that build exactly those widgets that are visible to the user.

积极可组合性

Aggressive composability

组合性是 Flutter 最为出众的一个特性。widget 通过组合其他 widget 的方式进行构建,并且这些 widget 自身由更基础的 widget 构建。比如,Padding 是一个 widget 而非其他 widget 的属性。因此,使用 Flutter 创建的用户界面是由多个 widget 组成的。

One of the most distinctive aspects of Flutter is its aggressive composability. widgets are built by composing other widgets, which are themselves built out of progressively more basic widgets. For example, Padding is a widget rather than a property of other widgets. As a result, user interfaces built with Flutter consist of many, many widgets.

widget 递归构建的底层是 RenderObjectwidget,它将在渲染树的底部创建子节点。渲染树是一种存储用户界面几何信息的数据结构,该几何信息在 布局 期间计算并在 绘制命中测试 期间使用。大多数 Flutter 开发者无需直接创建这些对象,而是使用 widget 来操纵渲染树。

The widget building recursion bottoms out in RenderObjectwidgets, which are widgets that create nodes in the underlying render tree. The render tree is a data structure that stores the geometry of the user interface, which is computed during layout and used during painting and hit testing. Most Flutter developers do not author render objects directly but instead manipulate the render tree using widgets.

为了支持 widget 层的积极可组合性, Flutter 在 widget 和树渲染层使用了大量的高效算法和优化措施,这些将在下面小节中进行介绍。

In order to support aggressive composability at the widget layer, Flutter uses a number of efficient algorithms and optimizations at both the widget and render tree layers, which are described in the following subsections.

次线性布局

Sublinear layout

使用大量 widget 及渲染对象并保持高性能的关键是使用高效的算法。其中最重要的是确定渲染对象几何空间(比如大小和位置)的布局算法的性能。其他一些工具包使用 O(N²) 或更糟糕的布局算法(例如,约束域中的不动点迭代)。 Flutter 的目标在于布局初始化的线性性能,及一般情况下更新现有布局的次线性布局性能。通常情况下,布局所花费的时间应该比对象渲染要多得多。

With a large number of widgets and render objects, the key to good performance is efficient algorithms. Of paramount importance is the performance of layout, which is the algorithm that determines the geometry (for example, the size and position) of the render objects. Some other toolkits use layout algorithms that are O(N²) or worse (for example, fixed-point iteration in some constraint domain). Flutter aims for linear performance for initial layout, and sublinear layout performance in the common case of subsequently updating an existing layout. Typically, the amount of time spent in layout should scale more slowly than the number of render objects.

Flutter 对每一帧执行一次布局操作,且布局算法仅在一次传递中完成。 约束信息通过父节点调用每个子节点的布局方法向下传递。子节点递归执行自身的布局操作,并在它们的布局方法中返回几何信息以便将其添加到渲染树中。需要注意的是,一旦渲染对象从布局中返回,该对象将不会被再次访问 1,直到下一帧布局的执行。该策略将可能存在的单独测量和布局传递合并为单次传递,因此,每个渲染对象在布局过程中最多被访问两次 2:一次在树的向下传递过程中,一次在树的向上传递过程中。

Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again1 until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice2 during layout: once on the way down the tree, and once on the way up the tree.

针对这个通用协议,Flutter 拥有多种实现。最常用的是 RenderBox,它以二维的笛卡尔坐标进行运算。在盒子布局中,约束是最小及最大宽高。在布局过程中,子节点通过选择这些边界内的大小来确定其几何信息。子节点在布局中返回后,由父节点确定该子节点在父坐标系中的位置 3。注意,子节点的布局并不取决于它的位置,这是因为它的位置直到它从布局中返回后才确定。因此父节点可以在无需重新计算子节点布局的情况下重新定位子节点的位置信息。

Flutter has several specializations of this general protocol. The most common specialization is RenderBox, which operates in two-dimensional, cartesian coordinates. In box layout, the constraints are a min and max width and a min and max height. During layout, the child determines its geometry by choosing a size within these bounds. After the child returns from layout, the parent decides the child’s position in the parent’s coordinate system3. Note that the child’s layout cannot depend on its position, as the position is not determined until after the child returns from the layout. As a result, the parent is free to reposition the child without needing to recompute its layout.

更广泛地讲,在布局期间,从父节点流向子节点的唯一信息是约束信息,从子节点流向父节点的唯一信息是几何信息。通过这些不变量可减少布局期间所需的工作量:

More generally, during layout, the only information that flows from parent to child are the constraints and the only information that flows from child to parent is the geometry. These invariants can reduce the amount of work required during layout:

这些优化措施的效果是,当渲染对象包含脏节点时,在布局过程中,只有这些节点以及它们周围子树的有限节点才允许被访问。

As a result of these optimizations, when the render object tree contains dirty nodes, only those nodes and a limited part of the subtree around them are visited during layout.

次线性 widget 构建

Sublinear widget building

Flutter 使用类似于布局的次线性算法来构建 widget。widget 构建完成后,它们将被保留了用户页面逻辑结构的 element 树 保存。 Element 树是非常有必要的,这是因为 widget 自身是不可变的,这意味着(其他情况除外),它们无法记住父(或子)节点与其他 widget 的关系。 Element 还保存了与 Stateful widget 相关联的 state 对象。

Similar to the layout algorithm, Flutter’s widget building algorithm is sublinear. After being built, the widgets are held by the element tree, which retains the logical structure of the user interface. The element tree is necessary because the widgets themselves are immutable, which means (among other things), they cannot remember their parent or child relationships with other widgets. The element tree also holds the state objects associated with stateful widgets.

由于用户输入(或来自其他地方的响应),比如开发者在关联的 state 对象上调用了 setState() 方法,element 可能会变脏。框架维护了一个脏 element 列表,使得 构建 过程可跳过干净的 element,直接跳转到脏的 element。构建过程中,信息在 element 树中向下 单向 传递,这意味着该阶段中每个 element 最多会被访问一次。一个 element 一旦被清洗,它将不会再次变脏,这是因为通过归纳,它所有的祖先 element 也都是干净的 4

In response to user input (or other stimuli), an element can become dirty, for example if the developer calls setState() on the associated state object. The framework keeps a list of dirty elements and jumps directly to them during the build phase, skipping over clean elements. During the build phase, information flows unidirectionally down the element tree, which means each element is visited at most once during the build phase. Once cleaned, an element cannot become dirty again because, by induction, all its ancestor elements are also clean4.

由于 widget 是不可变的,因此父节点使用相同的 widget 来重新构建 element,如果 element 没有将自己标记为脏,那么该 element 可立即从构建中返回,以切断构建的向下传递。另外,element 只需比较两个 widget 所引用的对象标识来确定新 widget 与旧 widget 是否相同。开发者可利用该优化实现投影模式,即 widget 包含了被存储为成员变量、在构建过程中预先构建的子 widget

Because widgets are immutable, if an element has not marked itself as dirty, the element can return immediately from build, cutting off the walk, if the parent rebuilds the element with an identical widget. Moreover, the element need only compare the object identity of the two widget references in order to establish that the new widget is the same as the old widget. Developers exploit this optimization to implement the reprojection pattern, in which a widget includes a prebuilt child widget stored as a member variable in its build.

构建过程中,Flutter 同时使用 Inheritedwidgets 来避免父链的遍历。如果 widget 经常遍历它们的父链,比如确定当前的主题颜色,那么构建阶段树的深底将变为 O(N²),由于 Flutter 的积极可组合性,其数量可能非常巨大。为了避免这些父链的遍历,框架通过在每个 element 上维护一个 Inheritedwidget 哈希表来向下传递 element 树中的信息。通常情况下,多个 element 引用相同的哈希表,并且该表仅在 element 引入新的 Inheritedwidget 时改变。

During build, Flutter also avoids walking the parent chain using Inheritedwidgets. If widgets commonly walked their parent chain, for example to determine the current theme color, the build phase would become O(N²) in the depth of the tree, which can be quite large due to aggressive composition. To avoid these parent walks, the framework pushes information down the element tree by maintaining a hash table of Inheritedwidgets at each element. Typically, many elements will reference the same hash table, which changes only at elements that introduce a new Inheritedwidget.

线性协调

Linear reconciliation

不同于传统做法,Flutter 没有使用树差异比较算法。相反,框架通过使用 O(N) 算法独立地检查每个 element 的子节点来决定是否重用该 element。子列表协调算法针对以下情况进行了优化:

Contrary to popular belief, Flutter does not employ a tree-diffing algorithm. Instead, the framework decides whether to reuse elements by examining the child list for each element independently using an O(N) algorithm. The child list reconciliation algorithm optimizes for the following cases:

通常的做法是从新旧子列表的头部和尾部开始对每一个 widget 的运行时类型和 key 进行匹配,这样就可能找到在两个列表中间所有不匹配子节点的(非空)范围。然后框架将旧子列表中该范围内的子项根据它的 key 放入一个哈希表中。接下来,框架将会遍历新的子列表以寻找该范围内能够匹配哈希表中的 key的子项。无法匹配的子项将会被丢弃并从头开始重建,匹配到的子项则使用它们新的 widget 进行重建。

The general approach is to match up the beginning and end of both child lists by comparing the runtime type and key of each widget, potentially finding a non-empty range in the middle of each list that contains all the unmatched children. The framework then places the children in the range in the old child list into a hash table based on their keys. Next, the framework walks the range in the new child list and queries the hash table by key for matches. Unmatched children are discarded and rebuilt from scratch whereas matched children are rebuilt with their new widgets.

树结构优化

Tree surgery

重用 element 对性能非常重要,这是因为 element 拥有两份关键数据:Stateful widget 的状态对象及底层的渲染对象。当框架能够重用 element 时,用户界面的逻辑状态信息是不变的,并且可以重用之前计算的布局信息,这通常可以避免遍历整棵子树。事实上,重用 element 是非常有价值的,因为 Flutter 支持 全局 树更新,以此保留状态和布局信息。

Reusing elements is important for performance because elements own two critical pieces of data: the state for stateful widgets and the underlying render objects. When the framework is able to reuse an element, the state for that logical part of the user interface is preserved and the layout information computed previously can be reused, often avoiding entire subtree walks. In fact, reusing elements is so valuable that Flutter supports non-local tree mutations that preserve state and layout information.

开发者可通过将 GlobalKey 与其中一个 widget 相关联来实施全局树更新。每个全局 key 在整个应用中都是唯一的,并使用特定于线程的哈希表进行注册。在构建过程中,开发者可以使用全局 key 将 widget 移动到 element 树的任意位置。框架将不会在该位置上重新构建 element,而是检查哈希表并将现有的 element 从之前的位置移动到新的位置,从而保留整棵子树。

Developers can perform a non-local tree mutation by associating a GlobalKey with one of their widgets. Each global key is unique throughout the entire application and is registered with a thread-specific hash table. During the build phase, the developer can move a widget with a global key to an arbitrary location in the element tree. Rather than building a fresh element at that location, the framework will check the hash table and reparent the existing element from its previous location to its new location, preserving the entire subtree.

重新构建的子树中的渲染对象能够保留它们的布局信息,这是因为布局约束是渲染树从父节点传递到子节点的唯一信息。子列表发生变化后,父节点将会被标记为脏,但如果新的父节点传递给子节点的布局约束与该子节点从旧的父节点接收到的相同,那么子节点可立即从布局中返回,从而切断布局的向下传递。

The render objects in the reparented subtree are able to preserve their layout information because the layout constraints are the only information that flows from parent to child in the render tree. The new parent is marked dirty for layout because its child list has changed, but if the new parent passes the child the same layout constraints the child received from its old parent, the child can return immediately from layout, cutting off the walk.

开发者广泛使用全局 key 和全局树更新来实现 hero transition 及导航等效果。

Global keys and non-local tree mutations are used extensively by developers to achieve effects such as hero transitions and navigation.

恒定因子优化

Constant-factor optimizations

除了上述算法优化,实现积极可组合还需依赖几个重要的恒定因子优化。这些优化对于上面所讨论的主要算法是非常重要的。

In addition to these algorithmic optimizations, achieving aggressive composability also relies on several important constant-factor optimizations. These optimizations are most important at the leaves of the major algorithms discussed above.

总的来说,这些优化对通过积极组合方式产生的大型树结构的性能产生了重大影响。

Taken together and summed over the large trees created by aggressive composition, these optimizations have a substantial effect on performance.

Separation of the Element and RenderObject trees

The RenderObject and Element (Widget) trees in Flutter are isomorphic (strictly speaking, the RenderObject tree is a subset of the Element tree). An obvious simplification would be to combine these trees into one tree. However, in practice there are a number of benefits to having these trees be separate:

无限滚动

Infinite scrolling

对于工具包来说,实现无限滚动列表是非常困难的。Flutter 支持基于 构造器 模式实现的简单无限滚动列表界面,其中 ListView 使用回调按需构建 widget,即它们只在滚动过程中才对用户可见。该功能需要 视窗感知布局按需构建 widget 的支持。

Infinite scrolling lists are notoriously difficult for toolkits. Flutter supports infinite scrolling lists with a simple interface based on the builder pattern, in which a ListView uses a callback to build widgets on demand as they become visible to the user during scrolling. Supporting this feature requires viewport-aware layout and building widgets on demand.

视窗感知布局

Viewport-aware layout

同 Flutter 中的大多数东西一样,可滚动的 widget 是基于组合模式构建的。可滚动 widget 的外部是一个 Viewport,这是一个拥有更大内部空间的盒子,这意味着它的子节点可以超出视窗口的边界并滚动到可视区域中。但是,视窗口没有 RenderBox 子节点,而是拥有被称为 sliver,实现了视窗感知协议的RenderSliver 子节点。

Like most things in Flutter, scrollable widgets are built using composition. The outside of a scrollable widget is a Viewport, which is a box that is “bigger on the inside,” meaning its children can extend beyond the bounds of the viewport and can be scrolled into view. However, rather than having RenderBox children, a viewport has RenderSliver children, known as slivers, which have a viewport-aware layout protocol.

sliver 布局协议中父节点向下传递给子节点的约束信息及接收到的几何信息的结构与盒子布局相同。但约束和几何数据在两个协议之间不同。在 sliver 协议中,子节点接收到的是关于视窗口的信息,这其中包含剩余的可见空间量。它们返回的几何数据支持各种滚动链接效果,包括可折叠标题及视差。

The sliver layout protocol matches the structure of the box layout protocol in that parents pass constraints down to their children and receive geometry in return. However, the constraint and geometry data differs between the two protocols. In the sliver protocol, children are given information about the viewport, including the amount of visible space remaining. The geometry data they return enables a variety of scroll-linked effects, including collapsible headers and parallax.

不同的 sliver 以不同的方式填充视窗口中的可用空间。比如,生成线性子列表的 sliver 按顺序排列每个子节点,直到 sliver 中无任何子节点或可用空间。同理,生成二维子节点网格的 sliver 仅填充网格中的可见区域。由于它们知道还有多大的可见空间,sliver 可以生成有限的子节点,即使它们可能生成无限的子节点。

Different slivers fill the space available in the viewport in different ways. For example, a sliver that produces a linear list of children lays out each child in order until the sliver either runs out of children or runs out of space. Similarly, a sliver that produces a two-dimensional grid of children fills only the portion of its grid that is visible. Because they are aware of how much space is visible, slivers can produce a finite number of children even if they have the potential to produce an unbounded number of children.

可组合 sliver 来创建特定的滚动布局和效果。比如,单个视窗口可以有一个折叠标题、一个线性列表和一个网格。所有这些 sliver 将按照 sliver 布局协议进行协作,只生成那些在视窗口实际可见的子节点,而不管这些子节点是否属于标题、列表或网格6

Slivers can be composed to create bespoke scrollable layouts and effects. For example, a single viewport can have a collapsible header followed by a linear list and then a grid. All three slivers will cooperate through the sliver layout protocol to produce only those children that are actually visible through the viewport, regardless of whether those children belong to the header, the list, or the grid6.

按需构建 widget

Building widgets on demand

如果 Flutter 拥有一个严格的从构建到布局,再到绘制的管道,那么前面的内容将不足以实现无限滚动列表,这是因为只有在布局阶段才能通过视窗口获取可用的空间信息。如果没有额外的机制,在布局阶段构建用于填充空间的 widget 已经太迟了。 Flutter 使用将管道的构建与布局交叉在一起的方式来解决这个问题。在布局阶段的任意时刻,只要这些 widget 是当前布局的渲染对象的子节点,框架就可以按需构建新的 widget。

If Flutter had a strict build-then-layout-then-paint pipeline, the foregoing would be insufficient to implement an infinite scrolling list because the information about how much space is visible through the viewport is available only during the layout phase. Without additional machinery, the layout phase is too late to build the widgets necessary to fill the space. Flutter solves this problem by interleaving the build and layout phases of the pipeline. At any point in the layout phase, the framework can start building new widgets on demand as long as those widgets are descendants of the render object currently performing layout.

只有严格控制构建及布局中消息传播的算法,才能实现构建和布局的交叉执行。也就是说,在构建过程中,消息只能沿构建树向下传递。当渲染对象进行布局时,布局遍历过程中并没有访问该渲染对象的子树,这意味通过子树构建的写入无法使到目前为止已进入布局计算过程的任何信息失效。无独有偶,一旦布局从渲染对象中返回,在当前布局过程中,该渲染对象将永远不会被再次访问,这意味后续布局计算生成的任何写入都不会使用于构建渲染对象的子树的信息失效。

Interleaving build and layout is possible only because of the strict controls on information propagation in the build and layout algorithms. Specifically, during the build phase, information can propagate only down the tree. When a render object is performing layout, the layout walk has not visited the subtree below that render object, which means writes generated by building in that subtree cannot invalidate any information that has entered the layout calculation thus far. Similarly, once layout has returned from a render object, that render object will never be visited again during this layout, which means any writes generated by subsequent layout calculations cannot invalidate the information used to build the render object’s subtree.

此外,线性协调及树结构优化对于在滚动过程中有效更新 element,以及当 element 在视窗口边缘滚动进出视图期间修改渲染树至关重要。

Additionally, linear reconciliation and tree surgery are essential for efficiently updating elements during scrolling and for modifying the render tree when elements are scrolled into and out of view at the edge of the viewport.

人机工程 API

API Ergonomics

速度只有在框架能够被有效使用时才有意义。为了引导设计更高可用性的 Flutter API, Flutter 已经在与开发者进行的广泛用户体验研究中进行了反复测试。这些研究有时证实了已有的设计决策,有时有助于引导功能的优先级,有时会改变 API 的设计方向。比如,Flutter 的 API 文档很多,用户体验的研究不仅证实了这些文档的价值,也同时强调了示例代码及说明性图表的重要性。

Being fast only matters if the framework can actually be used effectively. To guide Flutter’s API design towards greater usability, Flutter has been repeatedly tested in extensive UX studies with developers. These studies sometimes confirmed pre-existing design decisions, sometimes helped guide the prioritization of features, and sometimes changed the direction of the API design. For instance, Flutter’s APIs are heavily documented; UX studies confirmed the value of such documentation, but also highlighted the need specifically for sample code and illustrative diagrams.

本节将要讨论 Flutter API 设计中为提高可用性所做的一些决策。

This section discusses some of the decisions made in Flutter’s API design in aid of usability.

与开发者思维模式相匹配的专项 API

Specializing APIs to match the developer’s mindset

Flutter 中 widgetElementRenderObject 的基类节点不定义子类模型。该机制允许每个节点对适用于该节点的子模型进行定制化。

The base class for nodes in Flutter’s widget, Element, and RenderObject trees does not define a child model. This allows each node to be specialized for the child model that is applicable to that node.

大多数 widget 对象都有一个子 widget 对象,因此它只暴露了一个 child 参数。一些 widget 支持任意数量的子节点,并暴露了一个获取子节点列表的 children 参数。有些 widget 无任何子节点、不保留内存且无任何参数。同样的,RenderObjects 暴露特定于子模型的 API。 RenderImage 是一个没有子节点的叶子节点。 RenderPadding 只持有一个子节点,因此它有一个指向单个子节点的指针存储空间。 RenderFlex 接受任意数量的子节点,并通过链表对其进行管理。

Most widget objects have a single child widget, and therefore only expose a single child parameter. Some widgets support an arbitrary number of children, and expose a children parameter that takes a list. Some widgets don’t have any children at all and reserve no memory, and have no parameters for them. Similarly, RenderObjects expose APIs specific to their child model. RenderImage is a leaf node, and has no concept of children. RenderPadding takes a single child, so it has storage for a single pointer to a single child. RenderFlex takes an arbitrary number of children and manages it as a linked list.

在一些罕见情况下,将使用更复杂的子类模型。渲染对象 RenderTable 的构造函数需要使用二维数组来存储子节点,所以该类暴露了用于控制行和列数量的 getter 及 setter 方法,还有一些可以用 x、y 轴坐标来替换单个子节点的特殊方法,可通过提供一个新的子节点数组来添加新行,并用单个数组及列的个数来替换整个子节点列表。该对象并不像大多数渲染对象那样使用链表,而是使用可索引数组来实现。

In some rare cases, more complicated child models are used. The RenderTable render object’s constructor takes an array of arrays of children, the class exposes getters and setters that control the number of rows and columns, and there are specific methods to replace individual children by x,y coordinate, to add a row, to provide a new array of arrays of children, and to replace the entire child list with a single array and a column count. In the implementation, the object does not use a linked list like most render objects but instead uses an indexable array.

Chip widget 和 InputDecoration 对象具有与其控制中的插槽相匹配的字段。如果一个通用子模型将强制语义定义在子列表之上,比如将第一个子节点定义为前缀,第二个子节点定义为后缀,那么专用子模型允许使用特有的命名属性。

The Chip widgets and InputDecoration objects have fields that match the slots that exist on the relevant controls. Where a one-size-fits-all child model would force semantics to be layered on top of a list of children, for example, defining the first child to be the prefix value and the second to be the suffix, the dedicated child model allows for dedicated named properties to be used instead.

这种灵活性允许树中的每个子节点以其最常用的方式操作它的角色。很少有人想要在表格中插入一个单元格,从而导致其他所有单元格被环绕;同样的,很少有人想要通过索引而不是通过引用从 flex 行中删除子项。

This flexibility allows each node in these trees to be manipulated in the way most idiomatic for its role. It’s rare to want to insert a cell in a table, causing all the other cells to wrap around; similarly, it’s rare to want to remove a child from a flex row by index instead of by reference.

RenderParagraph 对象是最极端的情况:它有一个完全不同类型的子节点,TextSpan。在 RenderParagraph 的边界,RenderObject 树会被转换为 TextSpan 树。

The RenderParagraph object is the most extreme case: it has a child of an entirely different type, TextSpan. At the RenderParagraph boundary, the RenderObject tree transitions into being a TextSpan tree.

专门用于满足开发者期望的 API 的一切方法不仅适用于子模型。

The overall approach of specializing APIs to meet the developer’s expectations is applied to more than just child models.

专门存在一些琐碎的 widget,以便开发者在寻找问题解决方案时能够发现并使用它们。一旦知道如何使用 Expanded 和大小为零的 SizedBox 子部件,就可以轻松地为行或列添加空格,但你会发现这种模式是没有必要的,因为搜索 space 所找到的 Spacer,它是直接使用 ExpandedSizedBox 来达到同样的效果的。

Some rather trivial widgets exist specifically so that developers will find them when looking for a solution to a problem. Adding a space to a row or column is easily done once one knows how, using the Expanded widget and a zero-sized SizedBox child, but discovering that pattern is unnecessary because searching for space uncovers the Spacer widget, which uses Expanded and SizedBox directly to achieve the effect.

同理,可以通过在构建过程中不包含 widget 子树来轻松隐藏 widget 子树。但开发者通常希望有一个 widget 来执行该操作,因此 Visibility 的存在便是将此模式封装在一个简单的可重用 widget 中。

Similarly, hiding a widget subtree is easily done by not including the widget subtree in the build at all. However, developers typically expect there to be a widget to do this, and so the Visibility widget exists to wrap this pattern in a trivial reusable widget.

明确的参数

Explicit arguments

UI 框架往往拥有大量的属性,因此很少有开发者能够记住每个类的每个构造函数参数的作用。由于 Flutter 使用响应式编程范式,因此在 Flutter 中,构建方法通常会对构造函数进行多次调用。通过利用 Dart 的命名参数,Flutter 中的 API 能够使这些构建方法保持清晰易懂。

UI frameworks tend to have many properties, such that a developer is rarely able to remember the semantic meaning of each constructor argument of each class. As Flutter uses the reactive paradigm, it is common for build methods in Flutter to have many calls to constructors. By leveraging Dart’s support for named arguments, Flutter’s API is able to keep such build methods clear and understandable.

该模式已被扩展到任何具有多个参数(尤其是具有 boolean 类型参数)的方法,因此独立的 truefalse 值在方法调用中总是自我描述的。此外,为避免 API 中通常由双重否定所造成的困惑, boolean 类型的参数和属性始终以肯定的形式命名(比如,使用 enabled: true 而非 disabled: false)。

This pattern is extended to any method with multiple arguments, and in particular is extended to any boolean argument, so that isolated true or false literals in method calls are always self-documenting. Furthermore, to avoid confusion commonly caused by double negatives in APIs, boolean arguments and properties are always named in the positive form (for example, enabled: true rather than disabled: false).

参数陷阱

Paving over pitfalls

在 Flutter 框架中被大量使用的一项技术是定义不存在错误条件的 API。这样可以避免考虑整个错误类别。

A technique used in a number of places in the Flutter framework is to define the API such that error conditions don’t exist. This removes entire classes of errors from consideration.

比如插值函数允许插值的一端或两端为空,而不是将其定义为错误:两个空值之间的插值永远为空,并且从空值或空值插值等效于对指定类型进行零模拟插值。这意味着不小心将 null 传递给插值函数的开发者不会遇到错误,而是会得到一个合理结果。

For example, interpolation functions allow one or both ends of the interpolation to be null, instead of defining that as an error case: interpolating between two null values is always null, and interpolating from a null value or to a null value is the equivalent of interpolating to the zero analog for the given type. This means that developers who accidentally pass null to an interpolation function will not hit an error case, but will instead get a reasonable result.

一个更加微妙的例子是 Flex 布局算法。该布局给予 flex 渲染对象的空间被它的子节点所划分。因此 flex 的大小应该是整个可用空间。在最初的设计中提供无限空间将导致失败:这意味着 flex 应该是无限大且无用的布局设置。然而,通过对 API 的改造,在为 flex 对象提供无限空间时,渲染对象会调整自身大小来满足所需子节点的大小,从而减少可能出现的错误次数。

A more subtle example is in the Flex layout algorithm. The concept of this layout is that the space given to the flex render object is divided among its children, so the size of the flex should be the entirety of the available space. In the original design, providing infinite space would fail: it would imply that the flex should be infinitely sized, a useless layout configuration. Instead, the API was adjusted so that when infinite space is allocated to the flex render object, the render object sizes itself to fit the desired size of the children, reducing the possible number of error cases.

该方法也可用于避免使用允许创建不符合逻辑的数据的构造函数。例如,PointerDownEvent 的构造函数不允许将 PointerEventdown 属性设置为 false(这种情况是自相矛盾的);相反,构造函数没有关于字段 down 的参数,且将值始终设置为 true

The approach is also used to avoid having constructors that allow inconsistent data to be created. For instance, the PointerDownEvent constructor does not allow the down property of PointerEvent to be set to false (a situation that would be self-contradictory); instead, the constructor does not have a parameter for the down field and always sets it to true.

一般情况下,该方法用于为输入域中的所有值定义有效的解释。最简单的例子是 Color 的构造函数。相对于接受四个整型参数(分别用于表示红色、绿色、蓝色和 alpha),其中任何一个都可能超出范围,它的默认构造函数仅接受一个整数值,并定义每位的含义(例如,低八位代表红色),以便任何输入都是有效的颜色值。

In general, the approach is to define valid interpretations for all values in the input domain. The simplest example is the Color constructor. Instead of taking four integers, one for red, one for green, one for blue, and one for alpha, each of which could be out of range, the default constructor takes a single integer value, and defines the meaning of each bit (for example, the bottom eight bits define the red component), so that any input value is a valid color value.

一个更复杂的例子是 paintImage() 函数。该函数需要 11 个参数,其中一些具有相当宽泛的输入域,但它们都经过精心设计且大部分都能够彼此相交,因此很少出现无效组合。

A more elaborate example is the paintImage() function. This function takes eleven arguments, some with quite wide input domains, but they have been carefully designed to be mostly orthogonal to each other, such that there are very few invalid combinations.

积极报告错误

Reporting error cases aggressively

并非所有的错误都能被设计出来。对于那些遗漏的错误,在 debug 版本中,Flutter 通常会尝试尽早捕获并立即报告。它使用了大量的断言,对构造函数参数进行了详细的完整性检查,并监视其生命周期,一旦检测到不一致,它们会立即引发异常。

Not all error conditions can be designed out. For those that remain, in debug builds, Flutter generally attempts to catch the errors very early and immediately reports them. Asserts are widely used. Constructor arguments are sanity checked in detail. Lifecycles are monitored and when inconsistencies are detected they immediately cause an exception to be thrown.

这在某些情况下是极端情况:比如,在执行单元测试时,无论测试用例正在做什么,每个 RenderBox 子类都会主动地检查其内部大小调整方法是否满足内部大小调整契约。这有助于捕获可能无法执行的 API 错误。

In some cases, this is taken to extremes: for example, when running unit tests, regardless of what else the test is doing, every RenderBox subclass that is laid out aggressively inspects whether its intrinsic sizing methods fulfill the intrinsic sizing contract. This helps catch errors in APIs that might otherwise not be exercised.

当异常抛出时,它们会包含尽可能多的信息。 Flutter 中的一些错误会主动探测相关的堆栈跟踪信息,以确定实际错误最可能发生的位置。其他错误则通过相关树来确定坏数据的来源。最常见的错误包含详细说明(在某些情况下会包含避免错误的示例代码),或指向其他文档的链接。

When exceptions are thrown, they include as much information as is available. Some of Flutter’s error messages proactively probe the associated stack trace to determine the most likely location of the actual bug. Others walk the relevant trees to determine the source of bad data. The most common errors include detailed instructions including in some cases sample code for avoiding the error, or links to further documentation.

响应式

Reactive paradigm

可变的基于树结构的 API 受二元访问模式的影响:创建树的原始状态通常使用与后续更新完全不同的操作集。Flutter 的渲染层使用了这种范式,因为它是维护持久树的有效方法,是高效布局和绘制的关键所在。但这也意味着,与渲染层的直接交互是十分笨拙的,甚至极其容易出错。

Mutable tree-based APIs suffer from a dichotomous access pattern: creating the tree’s original state typically uses a very different set of operations than subsequent updates. Flutter’s rendering layer uses this paradigm, as it is an effective way to maintain a persistent tree, which is key for efficient layout and painting. However, it means that direct interaction with the rendering layer is awkward at best and bug-prone at worst.

Flutter 在 widget 层引入了一个使用响应式来操作底层渲染树的组合机制7。该 API 通过将树的创建和更新步骤整合到一个单一的树结构描述(构建)中,从而将树操作抽象出来,这包括:每次系统状态更新之后,开发者用于描述用户界面的新配置;框架对于新配置所需要进行的一系列树更新计算。

Flutter’s widget layer introduces a composition mechanism using the reactive paradigm7 to manipulate the underlying rendering tree. This API abstracts out the tree manipulation by combining the tree creation and tree mutation steps into a single tree description (build) step, where, after each change to the system state, the new configuration of the user interface is described by the developer and the framework computes the series of tree mutations necessary to reflect this new configuration.

插值

Interpolation

由于 Flutter 鼓励开发者描述与当前应用状态相匹配的界面配置,因此存在一种在这些配置之间执行隐式的动画机制。

Since Flutter’s framework encourages developers to describe the interface configuration matching the current application state, a mechanism exists to implicitly animate between these configurations.

例如,假设界面在状态 S1 由一个圆形组成,在状态 S2 时由一个正方形组成。如果没有动画机制,状态更改将导致不和谐的界面更改。隐式动画则允许界面在几个帧的时间里由圆形平滑地过渡到正方形。

For example, suppose that in state S1 the interface consists of a circle, but in state S2 it consists of a square. Without an animation mechanism, the state change would have a jarring interface change. An implicit animation allows the circle to be smoothly squared over several frames.

每个可执行隐式动画的特性都包含一个 Stateful widget,它用于记录输入的当前值,并在输入值改变时开始执行动画序列,并在指定的持续时间内从当前值转换为新值。

Each feature that can be implicitly animated has a stateful widget that keeps a record of the current value of the input, and begins an animation sequence whenever the input value changes, transitioning from the current value to the new value over a specified duration.

这是使用不可变对象的 lerp(线性插值)函数来实现的。每个状态(这里为圆形和正方形)代表一个配置中包含恰当设置(比如颜色、笔划宽度等)且知道如何绘制自己的不可变对象。在动画绘制中间步骤时,开始和结束值连同表示动画中点的 t 值一并传递给 lerp函数。其中 0.0 代表开始 start,1.0 代表结束 end8,并且该方法返回表示中间阶段的第三个不可变对象。

This is implemented using lerp (linear interpolation) functions using immutable objects. Each state (circle and square, in this case) is represented as an immutable object that is configured with appropriate settings (color, stroke width, etc) and knows how to paint itself. When it is time to draw the intermediate steps during the animation, the start and end values are passed to the appropriate lerp function along with a t value representing the point along the animation, where 0.0 represents the start and 1.0 represents the end8, and the function returns a third immutable object representing the intermediate stage.

对于从圆形到正方形的转换,lerp 函数将返回一个圆角正方形对象,其半径被描述为从 t 值导出的分数,使用 lerp 函数进行插值计算的颜色,以及使用 lerp 函数进行双倍插值计算的笔划宽度。该对象与圆形、正方形一样具有相同的接口实现,并且可以在请求时进行自我绘制。

For the circle-to-square transition, the lerp function would return an object representing a “rounded square” with a radius described as a fraction derived from the t value, a color interpolated using the lerp function for colors, and a stroke width interpolated using the lerp function for doubles. That object, which implements the same interface as circles and squares, would then be able to paint itself when requested to.

该技术允许状态机、状态到配置的映射、动画和插值机制以及与如何绘制每一桢完全分离的特定逻辑。

This technique allows the state machinery, the mapping of states to configurations, the animation machinery, the interpolation machinery, and the specific logic relating to how to paint each frame to be entirely separated from each other.

在 Flutter 中,该机制得到了广泛应用,无论是像 ColorShape 这样的基本类型,还是像 DecorationTextStyleTheme 这样更为复杂的类型,都是可以进行插值处理的。它们通常是由可插入组件构成的,并且插入更复杂的对象通常就像递归插入描述复杂对象的所有值一样简单。

This approach is broadly applicable. In Flutter, basic types like Color and Shape can be interpolated, but so can much more elaborate types such as Decoration, TextStyle, or Theme. These are typically constructed from components that can themselves be interpolated, and interpolating the more complicated objects is often as simple as recursively interpolating all the values that describe the complicated objects.

一些插值对象由类层次结构定义。比如,形状由 ShapeBorder 接口表示,并且存在多种形状类型,包括: BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。单一的 lerp 函数并不能了解所有可能的类型信息,因此接口定义了 lerpFromlerpTo 方法以替代静态的 lerp 方法。当被告知从形状 A 切换到 B 时,将首选询问 B 是否 lerpFrom A,如其答案为否,则询问 A 是否可以 lerpTo B (如两者的答案均为否,如果 t 的值小于 0.5 则返回 A,否则返回 B)。

Some interpolatable objects are defined by class hierarchies. For example, shapes are represented by the ShapeBorder interface, and there exists a variety of shapes, including BeveledRectangleBorder, BoxBorder, CircleBorder, RoundedRectangleBorder, and StadiumBorder. A single lerp function cannot have a priori knowledge of all the possible types, and therefore the interface instead defines lerpFrom and lerpTo methods, which the static lerp method defers to. When told to interpolate from a shape A to a shape B, first B is asked if it can lerpFrom A, then, if it cannot, A is instead asked if it can lerpTo B. (If neither is possible, then the function returns A from values of t less than 0.5, and returns B otherwise.)

这允许类层次结构的任意扩展,后续新增的能够在先前已知值与它们之间进行插值处理。

This allows the class hierarchy to be arbitrarily extended, with later additions being able to interpolate between previously-known values and themselves.

在某些情况下,插值本身不能被任何可用的类描述,并且定义一个私有类来描述中间状态。比如在 CircleBorderRoundedRectangleBorder 之间进行插值时就是如此。

In some cases, the interpolation itself cannot be described by any of the available classes, and a private class is defined to describe the intermediate stage. This is the case, for instance, when interpolating between a CircleBorder and a RoundedRectangleBorder.

该机制的另外一个优点是:它可以处理从中间态到新值的插值。比如,在圆形到正方形过渡的中途,形状可能再次改变,导致动画需要插值到一个三角形。只要该三角形类是 lerpFrom 圆形到正方形的中间类,就可以无缝进行转换。

This mechanism has one further added advantage: it can handle interpolation from intermediate stages to new values. For example, half-way through a circle-to-square transition, the shape could be changed once more, causing the animation to need to interpolate to a triangle. So long as the triangle class can lerpFrom the rounded-square intermediate class, the transition can be seamlessly performed.

结论

Conclusion

Flutter 一切都是 widget 的口号是围绕着通过组合 widget 来构建用户界面, widget 又由更为基础的 widget 构成。这种积极组合的结果是需要精心设计的算法和数据结构才能有效处理大量的 widget。通过一些额外的机制,这些数据结构还能使开发者轻松构建无限滚动列表,以便在 widget 可见时进行按需构建。

Flutter’s slogan, “everything is a widget,” revolves around building user interfaces by composing widgets that are, in turn, composed of progressively more basic widgets. The result of this aggressive composition is a large number of widgets that require carefully designed algorithms and data structures to process efficiently. With some additional design, these data structures also make it easy for developers to create infinite scrolling lists that build widgets on demand when they become visible.


脚注:

Footnotes:

1 至少对于布局来说。它可能会重新审视绘制、在必要时构建辅助功能树、以及必要时的命中测试。

1 For layout, at least. It might be revisited for painting, for building the accessibility tree if necessary, and for hit testing if necessary.

2 现实情况当然更复杂一些。有些布局涉及内部维度及基线测量,这涉及到相关子树的额外遍历���在最坏的情况下,使用积极缓存来降低潜在的二次性能)。但是,这些情况非常罕见。特别是在常见的 shrink-wrapping 情况下,根本不需要内部尺寸。

2 Reality, of course, is a bit more complicated. Some layouts involve intrinsic dimensions or baseline measurements, which do involve an additional walk of the relevant subtree (aggressive caching is used to mitigate the potential for quadratic performance in the worst case). These cases, however, are surprisingly rare. In particular, intrinsic dimensions are not required for the common case of shrink-wrapping.

3 严格来说,子节点的位置不是其 RenderBox 几何体的一部分,因此无需在布局期间进行实际计算。许多渲染对象隐式地将它们的单个子节点相对于它们自身的原点定位在 0,0 处,这根本不需要进行计算或存储。一些渲染对象避免计算它们子节点的位置直到最后可能需要的时刻(比如,在绘制过程中),以避免以后没有被绘制时的计算。

3 Technically, the child’s position is not part of its RenderBox geometry and therefore need not actually be calculated during layout. Many render objects implicitly position their single child at 0,0 relative to their own origin, which requires no computation or storage at all. Some render objects avoid computing the position of their children until the last possible moment (for example, during the paint phase), to avoid the computation entirely if they are not subsequently painted.

4 该规则有一个例外。正如 按需构建 widget 中所描述的,由于布局约束的变化,一些 widget 可以被重建。如果 widget 在同一帧中因与此无关的原因被标记为脏,同时也由于它受布局约束的影响,该 widget 将会被构建两次。该次冗余构建仅限于 widget 自身,并不会影响其后代节点。

4 There exists one exception to this rule. As discussed in the Building widgets on demand section, some widgets can be rebuilt as a result of a change in layout constraints. If a widget marked itself dirty for unrelated reasons in the same frame that it also is affected by a change in layout constraints, it will be updated twice. This redundant build is limited to the widget itself and does not impact its descendants.

5 键是一个可选的与 widget 相关联的不透明对象,它的相等操作符用于影响协调算法。

5 A key is an opaque object optionally associated with a widget whose equality operator is used to influence the reconciliation algorithm.

6 对于可访问性,并在 widget 构建及在窗口显示的过程中为应用提供几毫米的时间,视窗口会在可见 widget 的前后为几百个像素构建(但不进行绘制)widget。

6 For accessibility, and to give applications a few extra milliseconds between when a widget is built and when it appears on the screen, the viewport creates (but does not paint) widgets for a few hundred pixels before and after the visible widgets.

7 该方法首次在 Facebook 的 React 框架中得到了广泛使用。

7 This approach was first made popular by Facebook’s React library.

8 实际上,允许 t 值超过 0.0-1.0 的范围,这同样适用于某些曲线。比如 elastic 缓动曲线通过短暂的过冲来表示弹跳效应。插值逻辑通常可以在适当情况下推算出起始或结束点。对于某些类型,比如在插入颜色时,t 值被有效地固定到 0.0-1.0 的范围。

8 In practice, the t value is allowed to extend past the 0.0-1.0 range, and does so for some curves. For example, the “elastic” curves overshoot briefly in order to represent a bouncing effect. The interpolation logic typically can extrapolate past the start or end as appropriate. For some types, for example, when interpolating colors, the t value is effectively clamped to the 0.0-1.0 range.

不同平台操作体验的差异和适配

目录

适配哲学

Adaptation philosophy

平台适配通常有两种情形:

There are generally two cases of platform adaptiveness:

  1. 操作系统所特有的操作体验(例如文本编辑和滚动)。如果操作体验与操作系统不一致,则通常会被认为是“错误的”。

    Things that are behaviors of the OS environment (such as text editing and scrolling) and that would be ‘wrong’ if a different behavior took place.

  2. 使用 OEM 提供的 SDK 实现的功能体验(例如 iOS 常使用的选项卡, Android 使用 android.app.AlertDialog 显示一个提示窗口)。

    Things that are conventionally implemented in apps using the OEM’s SDKs (such as using parallel tabs on iOS or showing an android.app.AlertDialog on Android).

本文囊括了 Flutter 为解决情形 1 而提供的覆盖 Android 和 iOS 的自动适配。

This article mainly covers the automatic adaptations provided by Flutter in case 1 on Android and iOS.

对于情形 2,Flutter 提供了一些工具可以生成符合平台习惯的体验,但是不会根据平台自动适配,需要根据 App 设计来手工选择。更多有关的讨论,请访问 issue #8410 和这个文档 定义 Material/Cupertino widget 适配问题

For case 2, Flutter bundles the means to produce the appropriate effects of the platform conventions but doesn’t adapt automatically when app design choices are needed. For a discussion, see issue #8410 and the Material/Cupertino adaptive widget problem definition.

如果一个应用需要在 Android 和 iOS 不同架构上使用相同的代码,请参阅 platform_design 这份代码示例

For an example of an app using different information architecture structures on Android and iOS but sharing the same content code, see the platform_design code samples.

Page navigation

Flutter 分别为 Android 和 iOS 提供了各自平台的导航模式,并根据当前平台自动适配导航转场动画。

Flutter provides the navigation patterns seen on Android and iOS and also automatically adapts the navigation animation to the current platform.

Navigation transitions

Android 平台,默认提供的 Navigator.push() 转场动画模仿了 startActivity() 的动画,即一种自下而上的动画效果。

On Android, the default Navigator.push() transition is modeled after startActivity(), which generally has one bottom-up animation variant.

iOS 平台:

On iOS:

An animation of the bottom-up page transition on Android
Android 转场动画
An animation of the end-start style push page transition on iOS
iOS Push 转场动画
An animation of the bottom-up style present page transition on iOS
iOS Present 转场动画

不同平台的转场动画细节

Platform-specific transition details

Android 平台上,根据你的操作系统版本差异,有两种不同的转场动画:

On Android, two page transition animation styles exist depending on your OS version:

当在 iOS 平台上使用 Push 转场特效的时候, Flutter 内置的 CupertinoNavigationBarCupertinoSliverNavigationBar 会自动的给当前页下一页的子组件使用正确的动画效果(CupertinoNavigationBar 或者 CupertinoSliverNavigationBar)。

On iOS when the push style transition is used, Flutter’s bundled CupertinoNavigationBar and CupertinoSliverNavigationBar nav bars automatically animate each subcomponent to its corresponding subcomponent on the next or previous page’s CupertinoNavigationBar or CupertinoSliverNavigationBar.

An animation of the page transition on Android pre-Android P
Android 9 以前
An animation of the page transition on Android on Android P
Android 9 以后
An animation of the nav bar transitions during a page transition on iOS
iOS 导航栏

返回导航

Back navigation

Android 平台,通常操作系统的返回按钮触发的事件会发给 Flutter,并弹出 WidgetsApp 路由的最顶端。

On Android, the OS back button, by default, is sent to Flutter and pops the top route of the WidgetsApp’s Navigator.

iOS 平台,从屏幕边缘的轻扫手势会弹出路由的最顶端。

On iOS, an edge swipe gesture can be used to pop the top route.

A page transition triggered by the Android back button
Android 返回按钮
A page transition triggered by an iOS back swipe gesture
iOS 轻扫返回手势

滚动

Scrolling

滚动是不同平台提供独有体验非常重要的一环, Flutter 会根据当前的平台自动适配滚动体验。

Scrolling is an important part of the platform’s look and feel, and Flutter automatically adjusts the scrolling behavior to match the current platform.

物理仿真

Physics simulation

Android 和 iOS 平台都提供了非常复杂的滚动物理仿真,因而很难用语言来描述。通常来说, iOS 的滚动通常提供更多的分量和动态的阻力;而 Android 则更多的使用静态的阻力。所以,iOS 随着滚动慢慢的达到高速,且不会突然的停止,而且在慢速的时候显得更顺滑。

Android and iOS both have complex scrolling physics simulations that are difficult to describe verbally. Generally, iOS’s scrollable has more weight and dynamic friction but Android has more static friction. Therefore iOS gains high speed more gradually but stops less abruptly and is more slippery at slow speeds.

A soft fling where the iOS scrollable slid longer at lower speed than Android
突然慢慢滚动的效果比较
A medium force fling where the Android scrollable reached speed faster and stopped more abruptly after reaching a longer distance
突然较快的滚动效果比较
A strong fling where the Android scrollable reach speed faster and reached significantly more distance
突然强烈的滚动效果比较

滚动边界行为

Overscroll behavior

Android 平台,滚动达到边界的时候,会显示 滚动灰色指示 (具体颜色根据 Material 主题而有所不同)。

On Android, scrolling past the edge of a scrollable shows an overscroll glow indicator (based on the color of the current Material theme).

iOS 平台,滚动达到边界的时候,会显示一个 滚动边界 的弹簧效果。

On iOS, scrolling past the edge of a scrollable overscrolls with increasing resistance and snaps back.

Android and iOS scrollables being flung past their edge and exhibiting platform specific overscroll behavior
动态滚动边界效果比较
Android and iOS scrollables being overscrolled from a resting position and exhibiting platform specific overscroll behavior
静态滚动边界效果比较Static overscroll comparison

动量

Momentum

iOS 平台,不停的按相同方向滚动会产生动量叠加,从而连续滚动速度会越来越快。在 Android 平台上没有对应的行为。

On iOS, repeated flings in the same direction stacks momentum and builds more speed with each successive fling. There is no equivalent behavior on Android.

Repeated scroll flings building momentum on iOS
iOS 滚动动量

返回顶部

Return to top

iOS 平台,点击操作系统的状态栏,主要的滚动条控制器会滚动到顶部。 Android 没有对应的行为。

On iOS, tapping the OS status bar scrolls the primary scroll controller to the top position. There is no equivalent behavior on Android.

Tapping the status bar scrolls the primary scrollable back to the top
iOS 点击状态栏返回顶部

排版

Typography

当使用 Material package 的时候,排版会根据平台自动使用对应的字体。 Android 平台会使用 Roboto 字体,而 iOS 则会使用系统自带的 San Francisco 字体。

When using the Material package, the typography automatically defaults to the font family appropriate for the platform. On Android, the Roboto font is used. On iOS, the OS’s San Francisco font family is used.

当使用 Cupertino 包的时候,默认主题 会一直使用 San Francisco 字体。

When using the Cupertino package, the default theme always uses the San Francisco font.

San Francisco 字体的授权限制了它只能被用在运行于 iOS、macOS 和 tvOS 平台上的软件。因此当运行在 Android 平台的时候,即使强制覆盖系统平台为 iOS 或者使用 Cupertino 默认主题,都会使用对应的替代字体。

The San Francisco font license limits its usage to software running on iOS, macOS, or tvOS only. Therefore a fallback font is used when running on Android if the platform is debug-overridden to iOS or the default Cupertino theme is used.

Roboto font on Android
Android 平台 Robot 字体
San Francisco font on iOS
iOS 平台 San Francisco 字体

图标

Iconography

当使用 Material 包的时候,根据平台不同,图标的具体样式会有差别。举例来说,更多按钮的图标,Android 上是竖直的三个点而 iOS 是横着的三个点;退回按钮,iOS 是一个简单的 V 型标记,而 Android 平台,V 型标记有个短横线。

When using the Material package, certain icons automatically show different graphics depending on the platform. For instance, the overflow button’s three dots are horizontal on iOS and vertical on Android. The back button is a simple chevron on iOS and has a stem/shaft on Android.

Android appropriate icons
Android 平台图标
iOS appropriate icons
iOS 平台图标

The material library also provides a set of platform-adaptive icons through Icons.adaptive.

触摸反馈

Haptic feedback

Material 和 Cupertino 包在特定场景下都会自动触发符合平台特点的触摸反馈。

The Material and Cupertino packages automatically trigger the platform appropriate haptic feedback in certain scenarios.

例如,在文本输入框控件里面长按选中单词会在 Android 设备上会触发震动,而 iOS 不会。

For instance, a word selection via text field long-press triggers a ‘buzz’ vibrate on Android and not on iOS.

在 iOS 滚动选择器项目列表,会触发一个很轻的敲击音效,而 Android 则不会。

Scrolling through picker items on iOS triggers a ‘light impact’ knock and no feedback on Android.

文本编辑

Text editing

Flutter 会根据当前平台来适配正确的文本编辑体验。

Flutter also makes the below adaptations while editing the content of text fields to match the current platform.

键盘手势导航

Keyboard gesture navigation

Android 平台,在虚拟键盘空格键上可以通过左右轻扫来移动光标, Material 和 Cupertino 的文本输入框控件都支持该特性。

On Android, horizontal swipes can be made on the soft keyboard’s spacebar to move the cursor in Material and Cupertino text fields.

iOS 设备提供了 3D Touch 兼容,通过在虚拟键盘上使用长按并拖拽手势可以任意方向移动光标。 Material 和 Cupertino 都对这个功能提供了支持。

On iOS devices with 3D Touch capabilities, a force-press-drag gesture could be made on the soft keyboard to move the cursor in 2D via a floating cursor. This works on both Material and Cupertino text fields.

Moving the cursor via the space key on Android
Android 通过空格键移动光标
Moving the cursor via 3D Touch drag on the keyboard on iOS
iOS 通过 3D Touch 拖拽移动光标

文本选中工具栏

Text selection toolbar

Android 平台上使用 Material,在文本输入框里面选中文本会显示一个 Android 风格的文本选中工具栏。

With Material on Android, the Android style selection toolbar is shown when a text selection is made in a text field.

iOS 平台上使用 Material 或者在两个平台上都使用 Cupertino,在文本输入框里面选中文本会展示一个 iOS 风格的文本选中工具栏。

With Material on iOS or when using Cupertino, the iOS style selection toolbar is shown when a text selection is made in a text field.

Android appropriate text toolbar
Android 文本选中工具栏
iOS appropriate text toolbar
iOS 文本选中工具栏

点击手势

Single tap gesture

Android 平台使用 Material,在文本控件中点击会移动光标到点击处。

With Material on Android, a single tap in a text field puts the cursor at the location of the tap.

同时,光标会有一个可移动的把手,随后可以通过这个把手移动光标。

A collapsed text selection also shows a draggable handle to subsequently move the cursor.

iOS 平台使用 Material 或者在两个平台都使用 Cupertino,在文本空间中点击,会把光标移动到点击处最近的单词末尾。

With Material on iOS or when using Cupertino, a single tap in a text field puts the cursor at the nearest edge of the word tapped.

在 iOS 平台上,光标是没有把手的。

Collapsed text selections don’t have draggable handles on iOS.

Moving the cursor to the tapped position on Android
Android 点击
Moving the cursor to the nearest edge of the tapped word on iOS
iOS 点击

长按手势

Long-press gesture

Android 平台使用 Material,在单词上长按会选中单词,并在释放长按的时候显示文本选中工具栏。

With Material on Android, a long press selects the word under the long press. The selection toolbar is shown upon release.

iOS 平台使用 Material 或者在两个平台都使用 Cupertino,长按会把光标放置到长按的位置,并在释放长按的时候显示文本选中工具栏。

With Material on iOS or when using Cupertino, a long press places the cursor at the location of the long press. The selection toolbar is shown upon release.

Selecting a word via long press on Android
Android 长按
Selecting a position via long press on iOS
iOS 长按

长按并拖放手势

Long-press drag gesture

Android 平台上使用 Material,长按并拖拽会选中更多单词。

With Material on Android, dragging while holding the long press expands the words selected.

iOS 平台使用 Material 或者在两个平台都使用 Cupertino,长按并拖拽会移动光标。

With Material on iOS or when using Cupertino, dragging while holding the long press moves the cursor.

Expanding word selection via long press drag on Android
Android 长按并拖放
Moving the cursor via long press drag on iOS
iOS 长按并拖放

双击手势

Double tap gesture

Android 和 iOS 平台上,双击选中一个单词都会收到双击手势事件,并显示文本选中工具栏。

On both Android and iOS, a double tap selects the word receiving the double tap and shows the selection toolbar.

Selecting a word via double tap on Android
Android 双击
Selecting a word via double tap on iOS
iOS 双击

学习 Flutter 的视频列表与在线课程

目录

如果你是一个视觉型学习者 (visual learner),那么这些由 Google 或者社区(非 Google 官方)制作的 Flutter 的视频可能会对你有所帮助。

These Flutter videos, produced both internally at Google and by the Flutter community, might help if you are a visual learner.

这个页面仅列举部分 Flutter 视频,其他 Flutter 相关的视频可能会有很多。

Note that many people make Flutter videos. This page shows some that we like, but there are many others.


系列

Series

如下是一些 flutterdev YouTube channel 的系列视频。

Here are some of the series offered on the flutterdev YouTube channel.

Flutter Engage 2021

Flutter Engage 2021 是一个为期一天的活动,并正式发布了 Flutter 2。

Flutter Engage 2021 was a day-long event that officially launched Flutter 2.

查看 Flutter Engage 2021 主要亮点:

Check out the Flutter Engage 2021 highlights reel:

Watch recordings of the sessions on the Flutter YouTube channel:

Keynote
Flutter Engage 2021 playlist

Watch recordings of the sessions offered by the Flutter community:

Flutter Engage community talks playlist

Decoding Flutter

This series focuses on tips, tricks, best practices, and general advice on getting the most out of Flutter. In the comments section for this series, you can submit suggestions future episodes.

Introducing Decoding Flutter Decoding Flutter playlist

Flutter in focus

5 - 10 分钟左右的 Flutter 教程视频。

Five-to-ten minute tutorials (more or less) on using Flutter.

Introducing Flutter in focus
Flutter in focus playlist

每周 Flutter Widgets

Flutter widget of the week

每周一分钟,为你种草一个 Flutter widget!

Do you have 60 seconds? Each one-minute video highlights a Flutter widget.

Introducing widget of the week
Flutter widget of the week playlist

Flutter package of the week

你是否有几分钟时间,让我们用这些视频为你介绍一些精选 Flutter package。

If you have another minute (or two), each of these videos highlights a Flutter package.

The Boring Flutter show

This series features Flutter programmers coding, unscripted and in real time. Mistakes, solutions (some of them correct), and snazzy intro music included.

介绍 The Boring Flutter Show

Introducing the Boring Flutter show

The Boring Flutter show playlist

会议讲座

Conference talks

以下是各种会议上最近的一些Flutter会谈。

Here are a some recent Flutter talks given at various conferences.

Conference talks playlist

Flutter 开发者成功故事

Flutter developer stories

视频展示了艾比路工作室 (Abbey Road Studio)、汉密尔顿 (Hamilton) 和阿里巴巴 (Alibaba) 等开发者 / 公司如何使用 Flutter 开发出百万级别下载量,引人注目的应用程序。

Videos showing how various customers, such as Abbey Road Studio, Hamilton, and Alibaba, have used Flutter to create beautiful compelling apps with millions of downloads.

Flutter: 开发者成功故事播放列表

Flutter developer stories playlist

在线课程

Online courses

通过这些免费的课程学习如何构建 Flutter 应用。在注册课程之前,请验证它的内容是否及时更新,例如 Dart 的空安全代码等。这些课程按照字幕排序。若想要加入你的课程,请 提交一个 PR

Learn how to build Flutter apps with these video courses. Before signing up for a course, verify that it includes up-to-date information, such as null safe Dart code. These courses are listed alphabetically. To include your course, submit a PR:

Flutter 中文社区教程

Flutter 中文社区教程由社区的开发者投稿,内容同步发布到 flutter.cn 网站以及「Flutter 社区」的各个社交平台。本项目内部测试中,筹备完成后会开放投稿。

基础要求(待完善):

注意事项(待完善):

已发布的文章:

Flutter 技术分享

性能相关

Flutter 上的内存泄漏监控

路由和状态管理

设计模式

工程实践

Flutter x TensorFlow

Dart 技术分享

Dart 语言

Flutter 相关书籍

Here’s a collection of books about Flutter, in alphabetical order. If you find another one that we should add, file an issue and (optionally) submit a PR (sample) to add it yourself.

The following sections have more information about each book.

Beginning App Development with Flutter

Beginning App Development with Flutter

by Rap Payne

Easy to understand starter book. Currently the best-selling and highest rated Flutter book on Amazon.

Beginning Flutter: A Hands On Guide to App Development

Beginning Flutter: A Hands On Guide to App Development

by Marco L. Napoli

Build your first app in Flutter - no experience necessary.

Flutter Apprentice

Flutter Apprentice

by Kevin David Moore, Michael Katz, and Vincent Ngo

Build for both iOS and Android With Flutter! With Flutter and Flutter Apprentice, you can achieve the dream of building fast applications, faster.

Flutter Complete Reference

Flutter Complete Reference

by Alberto Miola

Create beautiful, fast and native apps for any device. You’ll learn about the Dart programming language (version 2.10, with null safety support) and the Flutter framework (version 1.20).While reading the chapters, you’ll find a lot of good practices, tips and performance advices to build high quality products.

Flutter: Développez vos applications mobiles multiplateformes avec Dart

Flutter: Développez vos applications mobiles multiplateformes avec Dart

by Julien Trillard

Ce livre sur Flutter s’adresse aux développeurs, initiés comme plus aguerris, qui souhaitent disposer des connaissances nécessaires pour créer de A à Z des applications mobiles multiplateformes avec le framework de Google

Flutter for Beginners

Flutter for Beginners

by Alessandro Biessek

A step-by-step guide to learning Flutter and Dart 2 for creating Android and iOS mobile applications.

Flutter in Action

Flutter in Action

by Eric Windmill

Flutter in Action teaches you to build professional-quality mobile applications using the Flutter SDK and the Dart programming language.

Flutter Libraries We Love

Flutter Libraries We Love

by Codemagic

“Flutter libraries We Love” focuses on 11 different categories of Flutter libraries. Each category lists available libraries as well as a highlighted library that is covered in more detail – including pros and cons, developer’s perspective, and real-life code examples. The code snippets of all 11 highlighted libraries support Flutter 2 with sound null safety.

Flutter Succinctly

Flutter Succinctly

by Ed Freitas

App UI in Flutter-from Zero to Hero.

Google Flutter Mobile Development Quick Start Guide

Google Flutter Mobile Development Quick Start Guide

by Prajyot Mainkar and Salvatore Girodano

A fast-paced guide to get you started with cross-platform mobile application development with Google Flutter

Learn Google Flutter Fast

Learn Google Flutter Fast

by Mark Clow

Learn Google Flutter by example. Over 65 example mini-apps.

Practical Flutter

Practical Flutter

by Frank Zammetti

Improve your Mobile Development with Google’s latest open source SDK.

Programming Flutter

Programming Flutter

by Carmine Zaccagnino

Native, Cross-Platform Apps the Easy Way. Doesn’t require previous Dart knowledge.

官方博客

目录

本页面将会归档所有 Flutter 官方博客的中文版内容:

2019 年

2020 年

2021 年

上述内容的刊登均以获得发布许可。

常见问题与解答

目录

简介

Introduction

本页面收集了关于 Flutter 一些大家常见问题的解答。你可能还会想要查看下面一些特别的答疑:

This page collects some common questions asked about Flutter. You might also check out the following specialized FAQs:

什么是 Flutter?

What is Flutter?

Flutter 是 Google 的便携式 UI 工具包,帮助你在移动、web、桌面端创造高质量的绝妙原生体验的应用。 Flutter 可以和世界上的开发人员和开发组织广泛使用的那些现存代码一起使用,并且是开源的、免费的。

Flutter is Google’s portable UI toolkit for crafting beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

哪些人会用到 Flutter?

Who is Flutter for?

对于用户来说,Flutter 将美妙的应用带到了生活中。

For users, Flutter makes beautiful apps come to life.

对于开发者来说,Flutter 降低了应用开发的入门门槛。它加速了应用开发的过程,减少了跨平台开发的成本以及复杂度。

For developers, Flutter lowers the bar to entry for building apps. It speeds app development and reduces the cost and complexity of app production across platforms.

对于设计师来说,Flutter 提供了一个能够实现高保真度用户体验的画布。 Fast 公司评价 Flutter 是 一个设计灵感的源泉,提供了将概念转换为生产代码的能力,却没有典型的框架强加的妥协。 Flutter 同时也是一个能提高生产力的原型工具,可以通过 CodePen 与他人分享你的创意。

For designers, Flutter provides a canvas for high-end user experiences. Fast Company described Flutter as one of the top design ideas of the decade for its ability to turn concepts into production code without the compromises imposed by typical frameworks. It also acts as a productive prototyping tool, with CodePen support for sharing your ideas with others.

对于工程主管以及雇主来说,Flutter 可以将不同平台的应用开发者,统一为一个 移动端、前端和桌面端应用程序 的团队,共同建立品牌,并在单个代码库中打造的多个平台的应用程序。 Flutter 加速了跨平台下开发以及同步发布进程的开发进度。

For engineering managers and businesses, Flutter allows the unification of app developers into a single mobile, web, and desktop app team, building branded apps for multiple platforms out of a single codebase. Flutter speeds feature development and synchronizes release schedules across the entire customer base.

我需要拥有怎样的开发经验才能使用 Flutter?

How much development experience do I need to use Flutter?

如果您熟悉面向对象概念 (类、方法、变量等) 和指令式编程概念 (循环、条件等) ,您会发现 Flutter 很容易上手。

Flutter is approachable to programmers familiar with object-oriented concepts (classes, methods, variables, etc) and imperative programming concepts (loops, conditionals, etc).

就我们亲历过的例子来说,编程经验并不丰富的人们一样可以学习并使用 Flutter 进行原型设计和应用开发。

We have seen people with very little programming experience learn and use Flutter for prototyping and app development.

我可以用 Flutter 构建怎样的应用?

What kinds of apps can I build with Flutter?

Flutter 设计为了让移动应用能够运行在 Android 与 iOS,以及在 web 和桌面端运行可交互式的应用。(请注意,桌面版支持目前已进入 beta 版本,但你还可以在 stable channel 中获取的 beta 版本的快照)

Flutter is designed to support mobile apps that run on both Android and iOS, as well as interactive apps that you want to run on your web pages or on the desktop. (Note that desktop support is in beta, but a snapshot of the beta is available on the stable channel.)

如果您的应用强烈需要表达出品牌个性,Flutter 会非常适合。不过,即便您想要打造的应用看起来像是股票平台那样复杂,也可以使用 Flutter 来构建。

Apps that need to deliver highly branded designs are particularly well suited for Flutter. However, you can also create pixel-perfect experiences that match the Android and iOS design languages with Flutter.

Flutter 的 软件包生态 支持绝大多数硬件(包括摄像头、GPS、网络以及储存)以及服务(例如支付、云储存、验证以及 广告)。

Flutter’s package ecosystem supports a wide variety of hardware (such as camera, GPS, network, and storage) and services (such as payments, cloud storage, authentication, and ads).

Flutter 可以构建功能齐全的应用,包括使用摄像头、地理位置、网络、存储、第三方 SDK 等。

You can build full-featured apps with Flutter, including camera, geolocation, network, storage, 3rd-party SDKs, and more.

谁创造了 Flutter?

Who makes Flutter?

Flutter 是一个开源项目,由 Google 和开发社区共同创造。

Flutter is an open source project, with contributions from Google and other companies and individuals.

谁在使用 Flutter?

Who uses Flutter?

Google 内部和外部的开发者使用 Flutter 为 Android 和 iOS 构建精美的原生应用。您可以访问 案例页面 来了解一些知名的开发者 / 组织。

Developers inside and outside of Google use Flutter to build beautiful natively-compiled apps for iOS and Android. To learn about some of these apps, visit the showcase.

Flutter 有哪些独到之处?

What makes Flutter unique?

Flutter 与大多数用来构建移动应用的工具不同,因为它既不使用 WebView,也不使用设备附带的 OEM Widget,而是使用自己的高性能渲染引擎来绘制 Widget。

Flutter is different than most other options for building mobile apps because it doesn’t rely on web browser technology nor the set of widgets that ship with each device. Instead, Flutter uses its own high-performance rendering engine to draw widgets.

Flutter 与其它工具的不同之处还在于,它只有一层简洁的 C/C++ 代码,在这之上,Flutter 使用 Dart (一种现代化的、简洁的面向对象语言) 实现其大部分系统功能 (布局、手势、动画、框架、Widget 等),这种语言使得开发者可以轻松地进行阅读、更改、替换或删除。这些特性都为开发者提供了巨大的系统控制权限,同时显著降低了访问大多数系统功能的门槛。

In addition, Flutter is different because it only has a thin layer of C/C++ code. Flutter implements most of its system (compositing, gestures, animation, framework, widgets, etc) in Dart (a modern, concise, object-oriented language) that developers can easily approach read, change, replace, or remove. This gives developers tremendous control over the system, as well as significantly lowers the bar to approachability for the majority of the system.

我需要使用 Flutter 来构建我的下一个应用吗?

Should I build my next production app with Flutter?

Flutter 1.0 已于 2018 年 12 月推出, Flutter 2 在 2021 年 3 月 3 日发布。至今为止,成千上万使用了 Flutter 的应用已经被安装到了数亿台设备中。请通过成功 案例页面 了解知名开发者们的成果。

Flutter 1.0 was launched on Dec 4th, 2018 and Flutter 2 on March 3rd, 2021. Since then, over 100,000 apps have shipped using Flutter to many hundreds of millions of devices. See some sample apps in the showcase.

Flutter 进行了高质量的持续交付更新,优化了稳定性、性能以及一些常见的用户需求。

Flutter ships updates on a roughly-quarterly cadence that improve stability and performance and address commonly-requested user features.

Flutter 能够为我们提供什么?

What does Flutter provide?

Flutter SDK 里有什么?

What is inside the Flutter SDK?

Flutter 包括了:

Flutter includes:

用 Flutter 开发时可以使用哪些编辑器或 IDE?

Does Flutter work with any editors or IDEs?

可以通过插件的方式使用 Android StudioIntelliJ IDEAVS Code 开发 Flutter 应用。请参阅 边界配置 以了解如何初始化,以及 Android Studio/IntelliJVS Code 如何使用 plugin 的小提示。

We provide plugins for Android Studio, IntelliJ IDEA, and VS Code. See editor configuration for setup details, and Android Studio/IntelliJ and VS Code for tips on how to use the plugins.

有关设置的详细信息,请参阅 编辑器配置文档,以及使用 Android Studio/IntelliJVS Code 插件的小提示。

See editor configuration for setup details, and Android Studio/IntelliJ and VS Code for tips on how to use the plugins.

您也可以在命令行中使用 flutter 命令,并配合能编辑 Dart 语言的编辑器 进行开发。

Alternatively, you can use the flutter command from a terminal, along with one of the many editors that support editing Dart.

Flutter 里存在开发框架吗?

Does Flutter come with a framework?

是的,Flutter 自带了现代化的开发框架,灵感正是来自 React。 Flutter 的框架旨在实现分层、可定制 (以及灵活的开发选项)。开发者可以选择仅使用框架的一部分,或是使用另外的框架。

Yes! Flutter ships with a modern react-style framework. Flutter’s framework is designed to be layered and customizable (and optional). Developers can choose to use only parts of the framework, or even replace upper layers of the framework entirely.

Flutter 里存在 Widget 吗?

Does Flutter come with widgets?

是的,Flutter 自带了一套 高品质的 Material Design 和 Cupertino (iOS 风格) Widget、布局和主题。当然,这些 Widget 只是一个起点。 Flutter 的设计目的就是,让您轻松创建自己的 Widget,或是定制现有的 Widget。

Yes! Flutter ships with a set of high-quality Material Design and Cupertino (iOS-style) widgets, layouts, and themes. Of course, these widgets are only a starting point. Flutter is designed to make it easy to create your own widgets, or customize the existing widgets.

Flutter 支持 Material Design 吗?

Does Flutter support Material Design?

是的,Flutter 和 Material 团队密切合作,完全支持 Material Theming。你可以通过 codelab 了解 Material 组件 (MDC) 主题定制: MDC-103 Flutter: Material Theming

Yes! The Flutter and Material teams collaborate closely, and Material is fully supported. A number of examples of this are shown in the MDC-103 Flutter: Material Theming codelab.

Flutter 带有测试框架吗?

Does Flutter come with a testing framework?

是的,Flutter 提供用于编写单元和集成测试的 API。了解更多有关 Flutter 测试的信息请查看 测试 Flutter 应用

Yes, Flutter provides APIs for writing unit and integration tests. Learn more about testing with Flutter.

我们使用自己的测试功能来测试我们的 SDK,每次提交代码前我们都会测量提交的 测试覆盖率

We use our own testing capabilities to test our SDK, and we measure our test coverage on every commit.

Flutter 是否附带调试工具?

Does Flutter come with debugging tools?

Flutter 本身附带了 调试工具(也称为 Dart DevTools)。你可以在 调试 FlutterFlutter DevTools 文档中了解更多信息。

Yes, Flutter comes with Flutter DevTools (also called Dart DevTools). For more information, see Debugging with Flutter and the Flutter DevTools docs.

Flutter 是否带有依赖注入 (dependency injection) 的框架?

Does Flutter come with a dependency injection framework?

我们并没有提供相关解决方案,但是这里有许多包提供了依赖注入或服务定位的能力,例如 injectableget_itkiwi

We don’t ship with an opinionated solution, but there are a variety of packages that offer dependency injection and service location, such as injectable, get_it, and kiwi.

技术篇

Technology

Flutter 是使用什么技术构建的?

What technology is Flutter built with?

Flutter 使用 C、C++、Dart 和 Skia (2D 渲染引擎) 构建。您可以参阅下面这张 架构图 来理解其主要构建。若您需要了解 Flutter 的分层架构,请阅读 架构概览

Flutter is built with C, C++, Dart, and Skia (a 2D rendering engine). See this architecture diagram for a better picture of the main components. For a more detailed description of the layered architecture of Flutter, read the architectural overview.

Flutter 如何在 Android 上运行我的代码?

How does Flutter run my code on Android?

引擎的 C 和 C++ 代码使用 Android 的 NDK 编译。 Dart 代码 (SDK 的和您写的) 都是预先 (ahead-of-time, AOT) 编译成本地 ARM 及 x86 库。这些库被包含在一个 Android “runner” 项目中,然后整套内容被编译成一个 APK。当应用启动时,它会加载 Flutter 库。任何渲染、输入或事件处理等都会 delegate 给编译好的 Flutter 和应用代码。这个工作机制与很多游戏引擎颇为相似。

The engine’s C and C++ code are compiled with Android’s NDK. The Dart code (both the SDK’s and yours) are ahead-of-time (AOT) compiled into native, ARM, and x86 libraries. Those libraries are included in a “runner” Android project, and the whole thing is built into an .apk. When launched, the app loads the Flutter library. Any rendering, input, or event handling, and so on, is delegated to the compiled Flutter and app code. This is similar to the way many game engines work.

调试模式时,Flutter 使用虚拟机 (VM) 来运行 Dart 代码(因此这时会显示 “Debug” 字样,以提醒开发者速度会稍微变慢),这样便可以启用有状态热重载 (Stateful Hot Reload),它能够让你无需重新编译整个应用就能看到代码变更带来的变化。当运行该模式时,你可以看到一个 “debug” banner 在你应用的右上角。请记住,这时的性能并不是最终发布应用时的性能。

During debug mode, Flutter uses a virtual machine (VM) to run its code in order to enable stateful hot reload, a feature that lets you make changes to your running code without recompilation. You’ll see a “debug” banner in the top right-hand corner of your app when running in this mode, to remind you that performance is not characteristic of the finished release app.

Flutter 如何在 iOS 上运行我的代码?{#run-ios}

How does Flutter run my code on iOS?

引擎的 C 和 C++ 代码使用 LLVM 编译。Dart 代码 (SDK 的和您的) 都是预先 (ahead-of-time, AOT) 编译成本地 ARM 库。这些库被包含在一个 iOS “runner” 项目中,然后整套内容被编译成一个 .ipa。当应用启动时,它会加载 Flutter 库。任何渲染、输入或事件处理等都会代理给编译好的 Flutter 和应用代码。这个工作机制与很多游戏引擎颇为相似。

The engine’s C and C++ code are compiled with LLVM. The Dart code (both the SDK’s and yours) are ahead-of-time (AOT) compiled into a native, ARM library. That library is included in a “runner” iOS project, and the whole thing is built into an .ipa. When launched, the app loads the Flutter library. Any rendering, input or event handling, and so on, are delegated to the compiled Flutter and app code. This is similar to the way many game engines work.

Flutter 是否会使用操作系统内置的 widget?

Does Flutter use my operating system’s built-in platform widgets?

不会。相反,Flutter 自己提供了一套 widget (包括 Material Design 和 iOS 风格的 Cupertino widget),由 Flutter 的框架和引擎负责管理和渲染。你可以在这里浏览 Flutter widget 目录

No. Instead, Flutter provides a set of widgets (including Material Design and Cupertino (iOS-styled) widgets), managed and rendered by Flutter’s framework and engine. You can browse a catalog of Flutter’s widgets.

我们希望最终能够产生出更高质量的应用。如果我们直接使用 OEM 自带的 widget,那么 Flutter 应用的质量和性能将受到这些 widget 质量的限制。

We believe that the end result is higher quality apps. If we reused the built-in platform widgets, the quality and performance of Flutter apps would be limited by the flexibility and quality of those widgets.

例如,在 Android 中,有一组硬编码的手势和固定的计算规则来区别它们。在 Flutter 中,您可以编写自己的手势识别器,它在 手势系统 中拥有最高的优先级。此外,由不同人创作的两个 widget 可以进行协调,以便消除手势的歧义。

In Android, for example, there’s a hard-coded set of gestures and fixed rules for disambiguating them. In Flutter, you can write your own gesture recognizer that is a first class participant in the gesture system. Moreover, two widgets authored by different people can coordinate to disambiguate gestures.

如今的应用设计趋势表明,很多设计师和用户都需要动效丰富的 UI,同时富有品牌表现力。为了实现这种级别的美学定制化设计,Flutter 在架构上就会倾向于直接驱动像素,而不是交给 OEM widget 来处理。

Modern app design trends point towards designers and users wanting more motion-rich UIs and brand-first designs. In order to achieve that level of customized, beautiful design, Flutter is architectured to drive pixels instead of the built-in widgets.

由于使用相同的渲染器、框架和 widget,就意味着您能更加轻松地同时发布 iOS 和 Android 版本应用,而无需耗费精力和成本来规划和同步两套独立的代码库和功能集。

By using the same renderer, framework, and set of widgets, it’s easier to publish for multiple platforms from the same codebase, without having to do careful and costly planning to align different feature sets and API characteristics.

另外,使用单一的语言、单个框架和同一组适用于所有 UI 的库(无论您的 UI 在每个移动平台上都各有不同还是基本一致),也有助于帮助您降低应用开发和维护成本。

By using a single language, a single framework, and a single set of libraries for all of your code (regardless if your UI is different for each platform or not), we also aim to help lower app development and maintenance costs.

我的移动 OS 更新并加入新的 widget 时会怎么样?

What happens when my mobile OS updates and introduces new widgets?

Flutter 团队密切关注来自 iOS 和 Android 的 widget 使用和需求情况,且会与社区合作,对新的 widget 提供构建支持。这些支持可能会以这些形式来提供给开发者: 较低层级的框架功能、新的可编辑组合的 widget,或全新的 widget 实现。

The Flutter team watches the adoption and demand for new mobile widgets from iOS and Android, and aims to work with the community to build support for new widgets. This work might come in the form of lower-level framework features, new composable widgets, or new widget implementations.

Flutter 的分层架构旨在支持众多 widget 库,我们鼓励并支持社区构建和维护 widget 库。

Flutter’s layered architecture is designed to support numerous widget libraries, and we encourage and support the community in building and maintaining widget libraries.

我的移动 OS 更新并加入新的平台功能时会怎么样?

What happens when my mobile OS updates and introduces new platform capabilities?

Flutter 的互操作 (interop) 和插件 (plugin) 系统旨在使开发者能够立即访问新的移动操作系统特性和功能。开发者不必等待 Flutter 团队提供新系统功能的访问接口,而是自己第一时间即可使用。

Flutter’s interop and plugin system is designed to allow developers to access new mobile OS features and capabilities immediately. Developers don’t have to wait for the Flutter team to expose the new mobile OS capability.

我能使用哪些操作系统开发 Flutter 应用?

What operating systems can I use to build a Flutter app?

Flutter 支持使用 Linux、Mac 和 Windows 进行开发。

Flutter supports development using Linux, macOS, ChromeOS, and Windows.

Flutter 是用哪种语言写成的?

What language is Flutter written in?

Dart 是一个现代化高度发展,并为终端应用专门优化的语言。底层图形框架和 Dart 虚拟机在 C/C++ 中实现。

Dart, a fast-growing modern language optimized for client apps. The underlying graphics framework and the Dart virtual machine are implemented in C/C++.

Flutter 为什么选择使用 Dart?

Why did Flutter choose to use Dart?

在最初的开发阶段,Flutter 团队调研了很多开发语言和运行时,最终在框架和小部件中采用了 Dart。 Flutter 主要基于四个维度进行评估,并同时考虑了框架作者、开发人员和终端用户的需求。我们发现许多语言都满足了一些要求,但 Dart 在我们所有的评估维度上都获得了高分,并且满足了我们的所有要求和标准。

During the initial development phase, the Flutter team looked at a lot of languages and runtimes, and ultimately adopted Dart for the framework and widgets. Flutter used four primary dimensions for evaluation, and considered the needs of framework authors, developers, and end users. We found many languages met some requirements, but Dart scored highly on all of our evaluation dimensions and met all our requirements and criteria.

Dart 运行时和编译器支持 Flutter 的两个关键功能的组合:基于 JIT 的高效开发,允许在具有类型的语言中进行形参更改,以及保持状态的热重载;还有 AOT 编译器,可产出高效的 ARM 代码,为生产部署带来快速启动和可观的性能。

Dart runtimes and compilers support the combination of two critical features for Flutter: a JIT-based fast development cycle that allows for shape changing and stateful hot reloads in a language with types, plus an Ahead-of-Time compiler that emits efficient ARM code for fast startup and predictable performance of production deployments.

此外,我们还有幸与 Dart 社区展开了密切合作,Dart 社区积极投入资源改进 Dart,以便在 Flutter 中更易使用。例如,当我们采用 Dart 时,该语言还没有用于生成原生二进制文件的 AOT 工具链,这些工具有助于实现稳定的高性能表现,但在 Dart 团队为 Flutter 构建了这些工具后,这个缺失已经不复存在了。同样,Dart VM 之前是针对吞吐量进行的优化,但团队现在正在针对延迟进行优化,这对于解决 Flutter 的工作负载更为重要。

In addition, we have the opportunity to work closely with the Dart community, which is actively investing resources in improving Dart for use in Flutter. For example, when we adopted Dart, the language didn’t have an ahead-of-time toolchain for producing native binaries, which is instrumental in achieving predictable, high performance, but now the language does because the Dart team built it for Flutter. Similarly, the Dart VM has previously been optimized for throughput but the team is now optimizing the VM for latency, which is more important for Flutter’s workload.

在评估时,Dart 在以下主要标准上得分很高:

Dart scores highly for us on the following primary criteria:

开发人员生产力
Flutter 的主要价值之一,是通过让开发人员用同一套代码,创建适用于 iOS 和 Android 的应用而节省开发资源。使用高生产力的语言加速开发,并提升 Flutter 的吸引力。这对于我们的框架团队和开发人员都很重要。 Flutter 本身的大部分内容所用的语言都和我们提供给用户的一样,所以我们要让十万行代码保持生产力,而不会牺牲框架和部件对我们开发人员的可达性和可读性。

Developer productivity
One of Flutter’s main value propositions is that it saves engineering resources by letting developers create apps for both iOS and Android with the same codebase. Using a highly productive language accelerates developers further and makes Flutter more attractive. This was very important to both our framework team as well as our developers. The majority of Flutter is built in the same language we give to our users, so we need to stay productive at 100k’s lines of code, without sacrificing approachability or readability of the framework and widgets for our developers.

面向对象
对于 Flutter 而言,我们需要一种适合创建可视化用户体验的语言。这个领域中沉淀了数十年的面向对象构建 UI 框架的经验。虽然我们可以使用非面向对象语言,但这意味着,为了解决几个难题,我们要 “重新发明轮子”。此外,绝大多数开发者都拥有面向对象开发的经验,因此可以更轻松地学习如何使用 Flutter 进行开发。

Object-orientation
For Flutter, we want a language that’s suited to Flutter’s problem domain: creating visual user experiences. The industry has multiple decades of experience building user interface frameworks in object-oriented languages. While we could use a non-object-oriented language, this would mean reinventing the wheel to solve several hard problems. Plus, the vast majority of developers have experience with object-oriented development, making it easier to learn how to develop with Flutter.

稳定可期的高性能表现
我们希望开发者能够通过 Flutter 创建快速而流畅的用户体验。为了实现这一点,我们需要能够在每个动画帧期间运行大量的最终开发者代码。这意味着我们需要的语言一方面既要拥有高性能,另一方面又需要避免因周期性的中断而影响帧率,即 “可期性”。

Predictable, high performance
With Flutter, we want to empower developers to create fast, fluid user experiences. In order to achieve that, we need to be able to run a significant amount of end-developer code during every animation frame. That means we need a language that both delivers high performance and predictable performance, without periodic pauses that would cause dropped frames.

快速内存分配
Flutter 框架使用的函数式流程,很大程度上依赖于下层的内存分配器高效地对小型的、短生命周期的内容进行内存分配。这个流程是使用支持这种分配机制的语言进行开发的,在缺少这个机制的语言中无法有效运作。

Fast allocation
The Flutter framework uses a functional-style flow that depends heavily on the underlying memory allocator efficiently handling small, short-lived allocations. This style was developed in languages with this property and doesn’t work efficiently in languages that lack this facility.

Flutter 可以运行那些没有直接或间接导入了 dart:mirrorsdart:html 的库。

Flutter can run Dart code that doesn’t directly or transitively import dart:mirrors or dart:html.

Flutter 引擎有多大?

How big is the Flutter engine?

2021 年 3 月,我们实测了一个 最简版本的 Flutter 应用 (即不含 Material 组件,只包含一个使用 flutter build apk --split-per-abi 构建的 Center widget 的 app)压缩且 Bundle 一个 release 的 APK, ARM64 下是 4.6 MB,ARM32 下是 4.3 MB。

In March 2021, we measured the download size of a minimal Flutter app (no Material Components, just a single Center widget, built with flutter build apk --split-per-abi), bundled and compressed as a release APK, to be approximately 4.3 MB for ARM32, and 4.8 MB for ARM64.

在 ARM32 下,核心的引擎大约占 3.4 MB,框架和应用的代码大约是 765 KB,许可证文件大约是 58 KB,必要的 Java 代码(classes.dex)是 120 KB。上述数据均为经过压缩处理之后的大小。

On ARM32, the core engine is approximately 3.4 MB (compressed), the framework + app code is approximately 765 KB (compressed), the LICENSE file is 58 KB (compressed), and necessary Java code (classes.dex) is 120 KB (compressed).

在 ARM64 下,核心的引擎大约占 4.0 MB,框架和应用的代码大约是 659 KB,许可证文件大约是 58 KB,必要的 Java 代码(classes.dex)是 120 KB。上述数据均为经过压缩处理之后的大小。

In ARM64, the core engine is approximately 4.0 MB (compressed), the framework + app code is approximately 659 KB (compressed), the LICENSE file is 58 KB (compressed), and necessary Java code (classes.dex) is 120 KB (compressed).

这些数字是由 AndroidStudio 内置的 apkanalyzer 实测得出。

These numbers were measured using apkanalyzer, which is also built into Android Studio.

在 iOS 平台上,跟据 App Store Connect 的数据,同一应用的发布 IPA 在 iPhone X 上的下载文件体积为 10.9 MB。 IPA 比 APK 大,主要是因为 Apple 加密了 IPA 中的二进制文件,使得压缩效率降低。(可以查看 iOS App Store Specific ConsiderationsQA1795 关于加密的部分)

On iOS, a release IPA of the same app has a download size of 10.9 MB on an iPhone X, as reported by Apple’s App Store Connect. The IPA is larger than the APK mainly because Apple encrypts binaries within the IPA, making the compression less efficient (see the iOS App Store Specific Considerations section of Apple’s QA1795).

Release 模式下引擎二进制产物将包含 LLVM 的中间语言表示 (bitcode)。 Xcode 将使用 bitcode 为 App Store 生成最终包含了最新的编译器优化和功能的二进制文件。 Profile 和 Debug 模式下的 Framework 中, bitcode 部分仅包含 bitcode marker,因此更能代表引擎的真实大小。无论你是否使用 bitcode, release 模式下增加的包大小都会在应用归档且发布到应用商店后,在构建的最终步骤里被移除。

The release engine binary includes LLVM IR (bitcode). Xcode uses this bitcode to produce a final binary for the App Store containing the latest compiler optimizations and features. The profile and debug frameworks contain only a bitcode marker, and are more representative of the engine’s actual binary size. Whether you ship with bitcode or not, the increased size of the release framework is stripped out during the final steps of the build. These steps happen after archiving your app and shipping it to the store.

当然,您的实际情况可能跟我们所说的有所不同,我们建议您测量自己的应用的体积。想要测量应用体积,请查看 测量你的应用体积

Of course, we recommend that you measure your own app. To do that, see Measuring your app’s size.

赋能

Capabilities

Flutter 应用会拥有怎样的性能表现?

What kind of app performance can I expect?

Flutter 应用会有很出色的性能。 Flutter 设计的目标就是帮助开发者轻松实现 60fps 的稳定帧率。 Flutter 应用通过本地编译的代码运行——不涉及解释过程。这也意味着 Flutter 应用启动会非常快捷。

You can expect excellent performance. Flutter is designed to help developers easily achieve a constant 60fps. Flutter apps run via natively compiled code—no interpreters are involved. This means that Flutter apps start quickly.

开发 Flutter 时的操作周期有多长?修改代码和看到界面内容更新之间会隔多久?

What kind of developer cycles can I expect? How long between edit and refresh?

Flutter 使用的是热重载式的开发操作周期。您在实机或者模拟器上都能实现亚秒级的修改和更新速度。

Flutter implements a hot reload developer cycle. You can expect sub-second reload times, on a device or an emulator/simulator.

另外,Flutter 的热重载是有状态的 (stateful),这意味着重新加载后 app 的状态会被保留。这样即使您修改的界面在应用很深的位置,重载后您也能直接看到修改后的该界面,而无需从应用首页开始重新操作。

Flutter’s hot reload is stateful, which means the app state is retained after a reload. This means you can quickly iterate on a screen deeply nested in your app, without starting from the home screen after every reload.

热重载 hot reload 相比较热重启 hot restart 的区别在哪里?

How is hot reload different from hot restart?

通过将更新的源代码文件注入到正在运行的 Dart VM(虚拟机)中来进行热重载。这不仅会添加新的类,还会向现有的类中添加方法和字段,并更改现有的函数。热重启后会将状态重置为应用程序的初始状态。

Hot reload works by injecting updated source code files into the running Dart VM (Virtual Machine). This doesn’t only add new classes, but also adds methods and fields to existing classes, and changes existing functions. Hot restart resets the state to the app’s initial state.

更多关于热重载的详细信息,请查看文档:使用热重载

For more information, see Hot reload.

我能把 Flutter 应用部署到哪里?

Where can I deploy my Flutter app?

您可以将 Flutter 应用编译并部署到 iOS 和 Android 平台,亦可部署到 web 平台,当然还有 桌面端(目前处于 beta 阶段)。一些比较激进的开发者已经部署了 Flutter 桌面应用,如果你觉得当前还不想要那么冒险,你也许想要等桌面支持合并到 stable channel。(然而我们已经将 beta 版桌面支持的快照发布在了 stable channel,供你们尝鲜。)

You can compile and deploy your Flutter app to iOS, Android, web, and desktop (in beta). While more adventurous developers are already deploying Flutter desktop apps, you might want to wait for desktop support to migrate to the stable channel if you are uncomfortable living on the edge. (However, a snapshot of beta desktop support is available on the stable channel, so you can try it out.)

Flutter 可以运行在哪些设备,哪些操作系统版本上?

What devices and OS versions does Flutter run on?

Flutter 能在 Web 上运行吗?

Does Flutter run on the web?

可以的,目前 stable channel 已经支持 web 平台了。你可以将已有的 Flutter 代码编译在 web 运行。更多详细信息,请参阅 Web 介绍

Yes, web support is available in the stable channel. You can compile existing Flutter code to work on the web. For more details, check out the web instructions.

我能使用 Flutter 构建桌面应用吗?

Can I use Flutter to build desktop apps?

可以的,Windows、macOS 以及 Linux 的桌面端的支持目前处于 beta 阶段,但已经在 stable channel 上提供了一份 beta 版本的快照。当前的进度记录在 桌面端支持 页面上查看。

Yes, desktop support is in beta for Windows, macOS, and Linux, but a snapshot of the beta is available on the stable channel.) The current progress is documented on the Desktop page.

我能在我现有的原生应用里使用 Flutter 吗?

Can I use Flutter inside of my existing native app?

是的,你可以在我们网上内的 混合应用 章节中学习。同时,请留意添加 多个 Flutter 页面或 view 体验版已经上线了。

Yes, learn more in the add-to-app section of our website. Also, note that experimental support for adding multiple Flutter screens or views is available.

请参考 这个文档,查看如何将 Flutter 加入现有的项目。

See the integration documentation in the add-to-app section of our website.

我能访问传感器、本地存储之类的平台服务和 API 吗?

Can I access platform services and APIs like sensors and local storage?

可以。Flutter 默认即为开发者提供了操作系统中 一些 平台专属服务和 API 的操作入口。但是,我们希望避免大多数跨平台 API 的“最小公约数”问题,因此我们不打算为所有本地服务和 API 构建跨平台的操作 API。

Yes. Flutter gives developers out-of-the-box access to some platform-specific services and APIs from the operating system. However, we want to avoid the “lowest common denominator” problem with most cross-platform APIs, so we don’t intend to build cross-platform APIs for all native services and APIs.

很多平台服务和 API 都在 Pub 站点中提供了 现成的代码包,我们可以根据 说明 使用它们,非常方便。

A number of platform services and APIs have ready-made packages available on pub.dev. Using an existing package is easy.

最后,我们鼓励开发者使用 Flutter 的异步消息传递系统来创建出 自己的平台 与第三方 API 的整合方案。开发者可以根据需要公开尽可能多 (或者尽可能少) 的平台 API,并构建最适合其项目的抽象层。

Finally, we encourage developers to use Flutter’s asynchronous message passing system to create your own integrations with platform and third-party APIs. Developers can expose as much or as little of the platform APIs as they need, and build layers of abstractions that are a best fit for their project.

我能对自带的 widget 进行扩展和定制吗?

Can I extend and customize the bundled widgets?

当然可以。Flutter widget 系统的设计思路就是让开发者可以轻松定制。

Absolutely. Flutter’s widget system was designed to be easily customizable.

Flutter 没有让每个 widget 都提供大量参数,而是采用了组合的方式。较大的 widget 是用较小的 widget 组合构建出来的,您可以重复使用它们,并以新颖的方式对其加以组合,从而生成自定义的 widget。例如,RaisedButton 没有继承自一个通用按钮 widget,而是将 Material widget 与 GestureDetector widget 组合在一起。 Material widget 负责视觉呈现,GestureDetector widget 则实现其交互。

Rather than having each widget provide a large number of parameters, Flutter embraces composition. Widgets are built out of smaller widgets that you can reuse and combine in novel ways to make custom widgets. For example, rather than subclassing a generic button widget, ElevatedButton combines a Material widget with a GestureDetector widget. The Material widget provides the visual design and the GestureDetector widget provides the interaction design.

如果您想要创建自定义设计的按钮,可以将负责视觉呈现的 widget 与提供交互的 GestureDetector 组合起来使用。例如,CupertinoButton 就采用了这种方法,将 GestureDetector 与其他几个负责表现视觉的 widget 进行组合。

To create a button with a custom visual design, you can combine widgets that implement your visual design with a GestureDetector, which provides the interaction design. For example, CupertinoButton follows this approach and combines a GestureDetector with several other widgets that implement its visual design.

这种组合策略使您可以最大限度地控制 widget 的可视化和交互逻辑,同时重复利用大量代码。在框架中,我们将复杂的 widget 分解为实现视觉、交互和动效的各部分。您可以按照自己喜欢的方式重新组合这些 widget,从而制作出自定义 widget 来完整传达出您的设计意图。

Composition gives you maximum control over the visual and interaction design of your widgets while also allowing a large amount of code reuse. In the framework, we’ve decomposed complex widgets to pieces that separately implement the visual, interaction, and motion design. You can remix these widgets however you like to make your own custom widgets that have full range of expression.

我为什么要在 iOS 和 Android 应用间共享布局代码?

Why would I want to share layout code across iOS and Android?

您可以选择为 iOS 和 Android 应用实现不同的布局。开发者可以在运行时检查移动操作系统的种类,并根据操作系统呈现不同的布局,但我们发现这种做法比较少见。

You can choose to implement different app layouts for iOS and Android. Developers are free to check the mobile OS at runtime and render different layouts, though we find this practice to be rare.

我们发现移动应用布局和设计正在不断发展,更趋于品牌设计的诉求,而且跨平台之间的呈现逐渐趋同。这意味着不少开发者会有很强的动力在 iOS 和 Android 上共享布局和 UI 代码。

More and more, we see mobile app layouts and designs evolving to be more brand-driven and unified across platforms. This implies a strong motivation to share layout and UI code across iOS and Android.

如今,在应用美学设计中,品牌表达和定制比严格遵循平台自己的美学更为重要。例如,应用设计通常需要自定义字体、颜色、形状、动效等,以便清楚地传达出其品牌独有的特性。

The brand identity and customization of the app’s aesthetic design is now becoming more important than strictly adhering to traditional platform aesthetics. For example, app designs often require custom fonts, colors, shapes, motion, and more in order to clearly convey their brand identity.

我们还发现,很多应用都在 iOS 和 Android 上采用了通用的布局模式。例如,您现在可以在 iOS 和 Android 上很方便地找到“底部导航”设计模式。移动平台上的设计理念似乎正在趋于一致。

We also see common layout patterns deployed across iOS and Android. For example, the “bottom nav bar” pattern can now be naturally found across iOS and Android. There seems to be a convergence of design ideas across mobile platforms.

我能与移动平台上的默认编程语言进行互操作吗?

Can I interop with my mobile platform’s default programming language?

可以,Flutter 支持调用 (包括集成) Android 上的 Java 或者 Kotlin 代码,或者 iOS 上的 ObjectiveC 或 Swift 代码。这是通过灵活的消息传递方式实现的, Flutter 应用可以使用 BasicMessageChannel 向移动平台收发消息。

Yes, Flutter supports calling into the platform, including integrating with Java or Kotlin code on Android, and ObjectiveC or Swift code on iOS. This is enabled via a flexible message passing style where a Flutter app might send and receive messages to the mobile platform using a BasicMessageChannel.

如果你想了解有关平台通道的更多信息,可以查阅 platform channels 相关文档。

Learn more about accessing platform and third-party services in Flutter with platform channels.

你也可以通过这个 示例项目,学习如何使用平台通道访问 iOS 和 Android 上的电池状态信息。

Here is an example project that shows how to use a platform channel to access battery state information on iOS and Android.

Flutter 包含反射 / 镜像系统吗?

Does Flutter come with a reflection / mirrors system?

不支持,Dart 的确是含有 dart:mirrors 库,能够提供类型反射。但是由于 Flutter 应用已经针对最终产物进行了预编译,并且控制二进制内容体积始终是现代移动应用需要面对的一个问题,所以我们禁用了 dart:mirrors。

No. Dart includes dart:mirrors, which provides type reflection. But since Flutter apps are pre-compiled for production, and binary size is always a concern with mobile apps, this library is unavailable for Flutter apps.

使用静态类型分析系统,我们可以移除任何不会用到的东西(”得益于 tree shaking 机制”)。如果你导入了一个巨大的 Dart 库,但仅仅用到了一个其中实现的一个两行的函数,那么你只需要付出这两行函数的代价,即便是这个 Dart 库中导入了非常多非常多的库。此保证仅 Dart 可以在编译期安全识别代码路径的情况下。目前,我们已找到其他满足特定需求的方法以提供更好的平衡,如代码生成。

Using static analysis we can strip out anything that isn’t used (“tree shaking”). If you import a huge Dart library but only use a self-contained two-line method, then you only pay the cost of the two-line method, even if that Dart library itself imports dozens and dozens of other libraries. This guarantee is only secure if Dart can identify the code path at compile time. To date, we’ve found other approaches for specific needs that offer a better trade-off, such as code generation.

我应该如何在 Flutter 实现国际化 (internationalization, i18n)、本地化 (localization, l10n) 和可访问性 (accessibility, a11y) ?

How do I do international­ization (i18n), localization (l10n), and accessibility (a11y) in Flutter?

关于国际化和本地化,请查看教程: Flutter 应用里的国际化

Learn more about i18n and l10n in the internationalization tutorial.

关于可访问性 / 无障碍使用,请查看文档:无障碍

Learn more about a11y in the accessibility documentation.

我如何为 Flutter 开发并行 (parallel) 和/或并发 (concurrent) 应用?

How do I write parallel and/or concurrent apps for Flutter?

Flutter 支持 isolate,一个个的 isolate 是 Flutter VM 里彼此独立的堆 (heap),可以并行运行 (通常以独立线程的形式实现)。 Isolate 之间通过异步收发消息来进行通信。

Flutter supports isolates. Isolates are separate heaps in Flutter’s VM, and they are able to run in parallel (usually implemented as separate threads). Isolates communicate by sending and receiving asynchronous messages.

你可以点击链接查看 在 Flutter 中使用 isolate 的示例

Check out an example of using isolates with Flutter.

我能在 Flutter 应用后台运行 Dart 代码吗?

Can I run Dart code in the background of an Flutter app?

可以,你可以在 iOS 和 Android 后台进程中运行 Dart 代码。有关更多信息,你可以查看在 Medium 上的文章: 使用 Flutter 插件和 Geofencing 在后台运行 Dart 代码

Yes, you can run Dart code in a background process on both iOS and Android For more information, see the free Medium article Executing Dart in the Background with Flutter Plugins and Geofencing.

我在 Flutter 里能使用 JSON/XML/protobuffers 等内容吗?

Can I use JSON/XML/protobuffers, etc. with Flutter?

当然可以。Pub 站点 提供了很多这样的代码库,包括 JSON, XML, protobufs 以及很多其他内容格式。

Absolutely. There are libraries on pub.dev for JSON, XML, protobufs, and many other utilities and formats.

有关在 Flutter 中使用 JSON 的详细介绍,你可以查看 使用 JSON 的教程

For a detailed writeup on using JSON with Flutter, check out the JSON tutorial.

我能用 Flutter 构建 3D (OpenGL) 应用吗?

Can I build 3D (OpenGL) apps with Flutter?

我们暂不支持通过 OpenGL ES 或类似的机制实现 3D。在 3D API 方面我们有一个长期的计划,但目前我们专注于呈现 2D。

Today we don’t support for 3D via OpenGL ES or similar. We have long-term plans to expose an optimized 3D API, but right now we’re focused on 2D.

我的 APK 或 IPA 为什么这么大?

Why is my APK or IPA so big?

通常,图像、声音文件、字体等资源在 APK 或 IPA 里占据了相当的比重。 Android 和 iOS 生态系统中有很多工具可以帮助您了解 APK 或 IPA 中的各种内容的比重情况。

Usually, assets including images, sound files, fonts, etc, are the bulk of an APK or IPA. Various tools in the Android and iOS ecosystems can help you understand what’s inside of your APK or IPA.

此外,请务必使用 Flutter 工具创建 APK 或 IPA 的_发布版本_。发布版本的体积通常_远_小于_调试_版本。

Also, be sure to create a release build of your APK or IPA with the Flutter tools. A release build is usually much smaller than a debug build.

如果你想学习更多有关如何发布版本的教程,可以查看 打包和发布到 Android 平台 以及 打包和发布到 iOS 平台。同时,查看 测量你的应用体积

Learn more about creating a release build of your Android app, and creating a release build of your iOS app. Also, check out Measuring your app’s size.

Flutter 应用能在 Chromebook 上运行吗?

Do Flutter apps run on Chromebooks?

我们注意到已经有 Flutter 应用运行在某些 Chromebook 上了。针对在 Chromebook 上运行 Flutter 的情况,我们有进行持续的跟踪,你可以查看 Flutter 运行在 Chromebook 上的问题追踪 来获得相关信息。

We have seen Flutter apps run on some Chromebooks. We are tracking issues related to running Flutter on Chromebooks.

Is Flutter ABI compatible?

Flutter 和 Dart 尚未提供且目前不会提供应用二进制接口 (ABI) 的支持。

Flutter and Dart do not offer application binary interface (ABI) compatibility. Offering ABI compatability is not a current goal for Flutter or Dart.

框架

Framework

为什么 build() 方法被放在 State 上,而不是 StatefulWidget 上?

Why is the build() method on State, rather than StatefulWidget?

将 Widget 的 build(BuildContext context) 方法放在 State 上,而不是将 Widget build(BuildContext context, State state) 方法放在 StatefulWidget 上,这个策略能让开发者在继承 StatefulWidget 时提供更多的灵活性,你可以在 API 文档中查看 关于 State.build 的讨论

Putting a Widget build(BuildContext context) method on State rather putting a Widget build(BuildContext context, State state) method on StatefulWidget gives developers more flexibility when subclassing StatefulWidget. You can read a more detailed discussion on the API docs for State.build.

Flutter 怎么没有标记语言 (markup language) 和语法?

Where is Flutter’s markup language? Why doesn’t Flutter have a markup syntax?

Flutter 的 UI 由指令式的面向对象语言构建,也就是 Dart。它也是 Flutter 框架的编写语言。Flutter 本身并不包含声明式的标记语言。

Flutter UIs are built with an imperative, object-oriented language (Dart, the same language used to build Flutter’s framework). Flutter doesn’t ship with a declarative markup.

我们发现将 UI 交给代码来动态构建会带来更多的灵活性。比如,我们发现固化的标记语言系统很难表达一个从视觉到行为都完全定制的 widget。

We found that UIs dynamically built with code allow for more flexibility. For example, we have found it difficult for a rigid markup system to express and produce customized widgets with bespoke behaviors.

另外,“代码优先”的开发也使得热重载以及动态环境适配等特性能更好地得以实现。

We have also found that our “code-first” better allows for features like hot reload and dynamic environment adaptations.

从根本上来讲,创造出一种能动态转化成 widget 的语言是可能的,毕竟构建方法说到底也还是代码,他们能做的事情很多,自然也包括将标记语言转化成 widget。

It’s possible to create a custom language that is then converted to widgets on the fly. Because build methods are “just code”, they can do anything, including interpreting markup and turning it into widgets.

我的应用运行时在右上角有一个 Debug 的标识,为什么?

My app has a Debug banner/ribbon in the upper right. Why am I seeing that?

默认情况下,flutter run 指令会使用 debug 编译配置。

By default flutter run command uses the debug build configuration.

Debug 编译配置会在一个 VM (Virtual Machine) 里运行您的 Dart 代码,从而提供更快速的开发操作周期,如 热重载。(如果是编译发布版本的话,则会使用 AndroidiOS 标准的工具链。)

The debug configuration runs your Dart code in a VM (Virtual Machine) enabling a fast development cycle with hot reload (release builds are compiled using the standard Android and iOS toolchains).

Debug 编译配置也会检查所有的断言 (assert),这会帮助您在开发时更早地发现错误,但这也会加大运行时的开销。您看到的 Debug 标识是告诉您这些检查目前是打开的状态。您可以通过在运行 flutter run 时附加 --profile 或者 --release 来跳过这些检查。

The debug configuration also checks all asserts, which helps you catch errors early during development, but imposes a runtime cost. The “Debug” banner indicates that these checks are enabled. You can run your app without these checks by using either the --profile or --release flag to flutter run.

如果您在使用 Flutter 的 IntelliJ 插件,您可以在 profile 或者 release 模式下启动应用,只需要在菜单里选择 Run > Flutter run in Profile Mode 或者 Release Mode 即可。

If your IDE uses the Flutter plugin, you can launch the app in profile or release mode. For IntelliJ, use the menu entries Run > Flutter Run in Profile Mode or Release Mode.

Flutter 框架采用了哪些编程范式?

What programming paradigm does Flutter’s framework use?

Flutter 是一个多范式的编程环境。过去几十年中许多编程技术都有在 Flutter 中使用。我们在选择范式时会考虑其适用性进行综合性的决策。以下列出的范式不分先后:

Flutter is a multi-paradigm programming environment. Many programming techniques developed over the past few decades are used in Flutter. We use each one where we believe the strengths of the technique make it particularly well-suited. In no particular order:

组合 (composition)
这也是 Flutter 的主要开发范式,将简单的、行为有限的小对象进行组合,从而实现更复杂的效果。绝大多数 Flutter widget 都是用这种方法构建的。比如 Material TextButton 类是基于 MaterialButton 类构建的,而这个类则是由 IconThemeInkWellPaddingCenterMaterialAnimatedDefaultTextStyle 以及 ConstrainedBox 组合而成的。而 InkWell 则是由 GestureDetector 组成, Material 则是由 AnimatedDefaultTextStyleNotificationListenerAnimatedPhysicalModel 组成。如此等等。

Composition
The primary paradigm used by Flutter is that of using small objects with narrow scopes of behavior, composed together to obtain more complicated effects, sometimes called aggressive composition. Most widgets in the Flutter widget library are built in this way. For example, the Material TextButton class is built using an IconTheme, an InkWell, a Padding, a Center, a Material, an AnimatedDefaultTextStyle, and a ConstrainedBox. The InkWell is built using a GestureDetector. The Material is built using an AnimatedDefaultTextStyle, a NotificationListener, and an AnimatedPhysicalModel. And so on. It’s widgets all the way down.

函数式编程 (functional programming)
整个应用都可以只用 StatelessWidget 来构建,它本质上就是一些方法,用来描述如何将参数传送给其他方法,以及在布局区域内计算布局以及绘制图像。当然这样的应用一般也不会包含状态,所以通常也无法进行交互。比如,Icon widget 就只是一个将其元素(颜色图标尺寸)罗列在布局区域内的方法。另外,当这个范式被重度使用时,则会使用不可变的数据结构,如整个 Widget 类及其派生,以及一些辅助类,如 RectTextStyle。另外,从一个较小的尺度来看的话, Dart 的 Iterable API 也重度使用了这个范式 (如 map, reduce, where 等方法),它在框架中经常被用来处理一系列的值。

Functional programming
Entire applications can be built with only StatelessWidgets, which are essentially functions that describe how arguments map to other functions, bottoming out in primitives that compute layouts or paint graphics. (Such applications can’t easily have state, so are typically non-interactive.) For example, the Icon widget is essentially a function that maps its arguments (color, icon, size) into layout primitives. Additionally, heavy use is made of immutable data structures, including the entire Widget class hierarchy as well as numerous supporting classes such as Rect and TextStyle. On a smaller scale, Dart’s Iterable API, which makes heavy use of the functional style (map, reduce, where, etc), is frequently used to process lists of values in the framework.

事件驱动编程 (event-driven programming)
用户的交互操作被包装成事件对象,这些对象发送给被各个 event handler 注册的回调方法。屏幕内容的更新使用的也是类似的回调机制。比如,做为动画系统构建基础的 Listenable 类,就采用了包含多个事件监听者的订阅模型。

Event-driven programming
User interactions are represented by event objects that are dispatched to callbacks registered with event handlers. Screen updates are triggered by a similar callback mechanism. The Listenable class, which is used as the basis of the animation system, formalizes a subscription model for events with multiple listeners.

面向类编程 (class-based programming,是面向对象编程的一种方式)
框架内绝大多数的 API 是由包含各种继承关系的类来组成的。我们在基本类中定义较高级别的 API,然后在其子类中对这些 API 进行特化处理。比如,我们的渲染对象就有一个基本类 RenderObject),它对坐标系的细节并不关心,但它的子类 RenderBox) 就引入了笛卡尔坐标系的概念(x/y坐标值,以及宽度高度的概念)。

Class-based object-oriented programming
Most of the APIs of the framework are built using classes with inheritance. We use an approach whereby we define very high-level APIs in our base classes, then specialize them iteratively in subclasses. For example, our render objects have a base class (RenderObject) that is agnostic regarding the coordinate system, and then we have a subclass (RenderBox) that introduces the opinion that the geometry should be based on the Cartesian coordinate system (x/width and y/height).

原型编程 (prototype-based programming,同样是面向对象编程的一种方式)
ScrollPhysics 类在运行时动态链接那些会组成滚动逻辑的实例。这就使得系统无需在编译时提前选择平台的情况下,也能组合出符合平台特性的页面滚动效果。

Prototype-based object-oriented programming
The ScrollPhysics class chains instances to compose the physics that apply to scrolling dynamically at runtime. This lets the system compose, for example, paging physics with platform-specific physics, without the platform having to be selected at compile time.

指令式编程 (imperative programming)
简单直白的指令式编程,通常和对象内封装的状态 (state) 搭配使用,这种范式能提供最符合直觉的解法。比如,测试就是使用指令式编程实现的,首先描述出测试的环境,然后给出测试需要满足的定量,最后开始步进,或者根据测试需要插入事件。

Imperative programming
Straightforward imperative programming, usually paired with state encapsulated within an object, is used where it provides the most intuitive solution. For example, tests are written in an imperative style, first describing the situation under test, then listing the invariants that the test must match, then advancing the clock or inserting events as necessary for the test.

响应式编程 (reactive programming)
Widget 和元素树有时候被描述为响应式的,因为随 widget 构造方法引入的新输入会随着其 build 方法传播给更低等级的 widget;而底层 widget 中出现的修改 (如响应用户的输入) 也会沿着结构树通过 event handler 向上传播。在整个框架中,函数-响应式以及指令-响应式的实现都有出现,具体取决于 widget 的功能需求。 Widget 的 build 方法如果只是包含其针对变化如何响应的表达式的话,就是函数-响应式 widget (如 Material Divider 类)。如果 widget 的 build 方法包含一系列构造子元素的表达式,用于描述该 widget 如何响应变化的话,那它就是指令响应式 widget (如 Chip 类)。

Reactive programming
The widget and element trees are sometimes described as reactive, because new inputs provided in a widget’s constructor are immediately propagated as changes to lower-level widgets by the widget’s build method, and changes made in the lower widgets (for example, in response to user input) propagate back up the tree via event handlers. Aspects of both functional-reactive and imperative-reactive are present in the framework, depending on the needs of the widgets. Widgets with build methods that consist of just an expression describing how the widget reacts to changes in its configuration are functional reactive widgets (for example, the Material Divider class). Widgets whose build methods construct a list of children over several statements, describing how the widget reacts to changes in its configuration, are imperative reactive widgets (for example, the Chip class).

声明式编程 (declarative programming)
Widget 的 build 方法通常都是一个单一表达式,它包含多级嵌套的构造函数,且使用 Dart 严格的声明式子集编写。这些嵌套的表达式可以与合适的标记语言互相转换。比如,UserAccountsDrawerHeader 这个 widget 就有一个很长的 build 方法 (20 多行),由一个嵌套的表达式构成。这种范式也可以和指令式混合使用,以实现某些很难用纯声明式的方法实现的 UI。

Declarative programming
The build methods of widgets are often a single expression with multiple levels of nested constructors, written using a strictly declarative subset of Dart. Such nested expressions could be mechanically transformed to or from any suitably expressive markup language. For example, the UserAccountsDrawerHeader widget has a long build method (20+ lines), consisting of a single nested expression. This can also be combined with the imperative style to build UIs that would be harder to describe in a pure-declarative approach.

泛型程序设计 (generic programming)
类型可以帮助开发者更早地抓到错误,基于这一点,Flutter 框架也采用了泛型开发。比如,State 类就是如此,其关联的 widget 就是类型参数,如此一来 Dart 分析器就能捕获到 state 和 widget 不匹配的情况。类似的,GlobalKey 类就接受一个类型参数,从而类型安全地访问一个 widget 的 state (会使用运行时检查)。 Route 接口也在被 popped 时接受类型参数,另外 List, Map, Set 这些集合也都如此,这样就可以在分析或者运行时尽早发现类型不匹配的错误。

Generic programming
Types can be used to help developers catch programming errors early. The Flutter framework uses generic programming to help in this regard. For example, the State class is parameterized in terms of the type of its associated widget, so that the Dart analyzer can catch mismatches of states and widgets. Similarly, the GlobalKey class takes a type parameter so that it can access a remote widget’s state in a type-safe manner (using runtime checking), the Route interface is parameterized with the type that it is expected to use when popped, and collections such as Lists, Maps, and Sets are all parameterized so that mismatched elements can be caught early either during analysis or at runtime during debugging.

并发 (concurrent programming)
Flutter 大量使用诸如 Future 等异步 API。比如,动画系统就会在动画执行完 future 时进行事件告知。同样的,图片加载系统也会使用 future 在加载完毕时进行告知。

Concurrent programming
Flutter makes heavy use of Futures and other asynchronous APIs. For example, the animation system reports when an animation is finished by completing a future. The image loading system similarly uses futures to report when a load is complete.

约束编程 (constraint programming)
Flutter 的布局系统使用了约束编程的简化形态来描述一个场景的几何性质。约束值 (比如一个笛卡尔矩形允许的最大 / 最小宽高值) 会从父元素传递给子元素,子元素最终选择一个能满足上面所有约束条件的最终尺寸。这种做法也使得 Flutter 能不依赖太多输入的情况下快速完成一个全新的布局。

Constraint programming
The layout system in Flutter uses a weak form of constraint programming to determine the geometry of a scene. Constraints (for example, for cartesian boxes, a minimum and maximum width and a minimum and maximum height) are passed from parent to child, and the child selects a resulting geometry (for example, for cartesian boxes, a size, specifically a width and a height) that fulfills those constraints. By using this technique, Flutter can usually lay out an entire scene with a single pass.

工程管理

Project

我该如何获得技术支持?

Where can I get support?

If you think you’ve encountered a bug, file it in our issue tracker. You might also use Stack Overflow for “HOWTO” type questions. For discussions, join our mailing list at flutter-dev@googlegroups.com or seek us out on Discord.

如果您觉得遇到 bug 了,请提交至我们的 问题追踪入口。我们也鼓励您在 Stack Overflow 中多多使用 “如何 (how to) …“来搜索解答。如果您希望直接与我们沟通,请使用我们的官方邮件地址 flutter-dev@googlegroups.com 或在 Discord 上向我们提问。

For more information, see our Community page.

我如何投入到 Flutter 开发社区?

How do I get involved?

Flutter 是开源的,我们鼓励您为此做出自己的贡献。您可以通过 问题追踪入口 来提交功能需求或者 bug 报告。

Flutter is open source, and we encourage you to contribute. You can start by simply filing issues for feature requests and bugs in our issue tracker.

我们也希望您加入我们的邮件讨论 flutter-dev@googlegroups.com,告诉我们您是如何使用 Flutter 的,以及打算用 Flutter 开发什么。

We recommend that you join our mailing list at flutter-dev@googlegroups.com and let us know how you’re using Flutter and what you’d like to do with it.

如果您打算为 Flutter 贡献代码,请先阅读 代码贡献指南,然后从 简单待修复问题 列表中寻找力所能及的问题开始入手。

If you’re interested in contributing code, you can start by reading our Contributing Guide and check out our list of easy starter issues.

最后,您可以与各个 Flutter 社区保持联系,更多相关信息,请查阅我们的 社区 页面。

Finally, you can connect with helpful Flutter communities. For more information, see our Community page.

Flutter 是开源的吗?

Is Flutter open source?

是的,Flutter 是开源的。您可以在 GitHub 上获取到它。

Yes, Flutter is open source technology. You can find the project on GitHub.

Flutter 以及其依存项目使用的是哪种软件许可协议?

Which software license(s) apply to Flutter and its dependencies?

Flutter 包含两个部分:一个使用动态链接二进制文件发行的引擎,以及引擎加载的 Dart 框架二进制文件。引擎使用了很多软件组件,且包含许多依存内容。完整的说明和依存清单请查看引擎的 许可协议

Flutter includes two components: an engine that ships as a dynamically linked binary, and the Dart framework as a separate binary that the engine loads. The engine uses multiple software components with many dependencies; view the complete list in its license file.

框架部分则自成一体,且 只有一份简单的许可协议

The framework is entirely self-contained and requires only one license.

另外,您使用的其他 Dart 代码包可能有其独有的许可协议。

In addition, any Dart packages you use might have their own license requirements.

我如何确定我的 Flutter 应用该显示哪些许可协议?

How can I determine the licenses my Flutter application needs to show?

您可以使用 API 来确定需要显示的许可协议。

There’s an API to find the list of licenses you need to show:

目前有哪些人在开发 Flutter?

Who works on Flutter?

我们都在参与 Flutter 开发!我们都知道 Flutter 是一个开源项目。目前 Flutter 中的大部分都是由 Google 的工程师来开发。如果您喜欢 Flutter 的话,我们希望您加入开发者社区并 做出贡献

We all do! Flutter is an open source project. Currently, the bulk of the development is done by engineers at Google. If you’re excited about Flutter, we encourage you to join the community and contribute to Flutter!

Flutter 有哪些指导原则?

What are Flutter’s guiding principles?

我们相信:

We believe that:

我们目前集中于以下三件事:

We are focused on three things:

功能控制
开发者应该能访问到系统所有层级的功能,且能获得全面的控制权。这也意味着:

Control
Developers deserve access to, and control over, all layers of the system. Which leads to:

性能表现
用户应该获得流畅、响应迅捷且没有卡顿的应用。这也意味着:

Performance
Users deserve perfectly fluid, responsive, jank-free apps. Which leads to:

精确实现
每一个人都应该获得精确、优美且富有表现力的移动应用体验。

Fidelity
Everyone deserves precise, beautiful, delightful app experiences.

Apple 会拒绝我的 Flutter 应用吗?

Will Apple reject my Flutter app?

我们无法代 Apple 发言,但已经有很多使用类似 Flutter 的其他技术开发的应用。实际上,Flutter 与 Unity 使用了近乎一致的底层架构模型, Apple store 中最著名的游戏也是使用它的引擎开发的。

We can’t speak for Apple, but their App Store contains many apps built with framework technologies such as Flutter. Indeed, Flutter uses the same fundamental architectural model as Unity, the engine that powers many of the most popular games on the Apple store.

Apple 最近评选的最佳设计应用也是使用 Flutter 开发的,其中包括 HamiltonReflectly

Apple has frequently featured well-designed apps that are built with Flutter, including Hamilton and Reflectly.

任何提交到 Apple store 的 Flutter 应用都应该遵守 Apple 的 规范

As with any app submitted to the Apple store, apps built with Flutter should follow Apple’s guidelines for App Store submission.

Flutter 设计文档

这一系列设计文档都是由 Flutter 工程师编写的。你可以按照以下说明添加新的设计文档:模板

This is a collection of design documents written by engineers working on Flutter. New design documents can be added by following the instructions in the template.

如何有效提出 Bug

目录

The instructions in this document detail the current steps required to provide the most actionable bug reports for crashes and other bad behavior. Each step is optional but will greatly improve how quickly issues are diagnosed and addressed. We appreciate your effort in sending us as much feedback as possible.

Create an issue on GitHub

Provide a minimal reproducible code sample

Create a minimal Flutter app that shows the problem you are facing, and paste it into the GitHub issue.

To create it you can use flutter create bug command and update the main.dart file.

Alternatively, you can use DartPad, which is capable of creating and running small Flutter apps.

If your problem goes out of what can be placed in a single file, for example you have a problem with native channels, you can upload the full code of the reproduction into a separate repository and link it.

Provide some Flutter diagnostics

[✓] Flutter (Channel stable, 1.22.3, on Mac OS X 10.15.7 19H2, locale en-US)
    • Flutter version 1.22.3 at /Users/me/projects/flutter
    • Framework revision 8874f21e79 (5 days ago), 2020-10-29 14:14:35 -0700
    • Engine revision a1440ca392
    • Dart version 2.10.3

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/me/Library/Android/sdk
    • Platform android-30, build-tools 29.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 12.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.2, Build version 12B5035g
    • CocoaPods version 1.9.3

[✓] Android Studio (version 4.0)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 50.0.1
    • Dart plugin version 193.7547
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.50.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.13.2

[✓] Connected device (1 available)
    • iPhone (mobile) • 00000000-0000000000000000 • ios • iOS 14.0

Run the command in verbose mode

Follow these steps only if your issue is related to the flutter tool.

Provide the most recent logs

Provide the crash report

了解 Dash

目录

这是 Dash:

This is Dash:

Dash 是 Dart 语言和 Flutter 框架的吉祥物。

Dash by herself

Dash is the mascot for the Dart language and the Flutter framework.

A sample of Dashatars

GIF of Dash fainting created for Flutter Engage

Dash 的由来?

How did it all start?

Shams Zakhour 从 2013 年 12 月在 Google 担任 Dart 作者的职务开始,她就一直在倡导推进 Dart 的吉祥物。在为 Java 撰写了 14 年的文档后,她发现 Java 的吉祥物 Duke 十分受欢迎,从而希望让 Dart 也拥有类似的东西。

但这个想法直到 2017 年一位 Flutter 工程师 Nina Chen 在内部邮件中再次提出,才取得了实质性的进展。时任副总裁 Joshy Joseph 批准了这个想法,并且让 2018 年度 Dart 会议 的组织者 Linda Rasmussen 进行落地。

As soon as Shams Zakhour started working as a Dart writer at Google in December 2013, she started advocating for a Dart mascot. After documenting Java for 14 years, she had observed how beloved the Java mascot, Duke, had become, and she wanted something similar for Dart.

Shams 在知晓这个计划后,立刻找到了 Linda,希望主导并推进这个项目,计划生产一些毛绒玩具。 Linda 早前已经设计出了一些草图,她将这些草图移交给了 Shams。有了这些草图后,Shams 找到了一家可以在截止日期前(赶在农历新年之前)合作的供应商,接着便开始制作毛绒玩具的规格。

But the idea didn’t gain momentum until 2017, when one of the Flutter engineers, Nina Chen, suggested it on an internal mailing list. The Flutter VP at the time, Joshy Joseph, approved the idea and asked the organizer for the 2018 Dart Conference, Linda Rasmussen, to make it happen.

是的,Dash 最初是 Dart 的吉祥物,而不是 Flutter 的。

Once Shams heard about these plans, she rushed to Linda and asked to own and drive the project to produce the plushies for the conference. Linda had already elicited some design sketches, which she handed off. Starting with the sketches, Shams located a vendor who could work within an aggressive deadline (competing with Lunar New Year), and started the process of creating the specs for the plushy.

以下是最初的原型和一些早期的模型:

That’s right, Dash was originally a Dart mascot, not a Flutter mascot.

Here are some early mockups and one of the first prototypes:

1st mockup made by Novicell.dk 2nd mockup by Squishable.com

Prototype 1 Playing with embroidery ideas

First prototype

初版原型的单只眼睛并不是对称的The first prototype had uneven eyes

为什么是一只蜂鸟?

Why a hummingbird?

最初,蜂鸟用来作为展示和网页用途。使用蜂鸟代表 Dart 是一门速度极快的语言。

Early on, a hummingbird image was created for the Dart team to use for presentations and the web. The hummingbird represents that Dart is a speedy language.

然而,又尖又有棱有角的蜂鸟无法作为毛茸茸的可爱玩具,所以我们最终选择了圆润的蜂鸟。

Early hummingbird drawing

Shams 为 Dash 指定了颜色的应用范围、尾巴的形状、一小簇头发、眼睛以及其他小细节。供应商将这些细则发给两个制造商后,他们在几周后送回了毛绒玩具的原型。

However, hummingbirds are pointed and angular and we wanted a cuddly plushy, so we chose a round hummingbird.

Shams specified which color would go where, the tail shape, the tuft of hair, the eyes…all the little details. The vendor sent the specs to two manufacturers who returned the prototypes some weeks later.

The first Dash prototypes The first Dash prototypes

在 2018 Dart 会议上介绍 Dash:Introducing Dash at the January 2018 Dart Conference:

在制造商推进制作的同时,Shams 毛绒玩具取了个名字:Dash,它其实是 Dart 项目的早期代号,而且不分性别,似乎更适合作为一只蜂鸟的名字。

许多箱 Dash 毛绒玩具赶在会议开始前送到了南加州。它们受到了大量 Dart 和 Flutter 爱好者的喜爱。

While the manufacturing process was proceeding, Shams chose a name for the plushy: Dash, because it was an early code name for the Dart project, it was gender neutral, and it seemed appropriate for a hummingbird.

Flutter 和 Dart 的演讲者都携带着 Dash 进行了演讲,所以 Dash 就成为了 Flutter 和 Dart 的吉祥物。

Many boxes of Dash plushies arrived in southern California just in time for the conference. They were eagerly adopted by Dart and Flutter enthusiasts.

The people have spoken, so Dash is now the mascot for Flutter and Dart.

Dash 1.0
Dash 1.0

Piles of Dashes awaiting conference goers

会议上的 DashConference swag

自 Dash 1.0 之后,我们又制作了两个版本的 Dash。市场部门针对 Dash 1.0 微调了 Dart 和 Flutter 的配色方案, Dash 2.0 对应了更新后的配色方案(移除了绿色)。 Dash 2.1 尺寸更小,并且调整了更多配色。小尺寸的 Dash 更易运输,并且更适合放在娃娃机中!

Since the creation of Dash 1.0, we’ve made two more versions. Marketing slightly changed the Dart and Flutter color scheme after Dash 1.0 was created, so Dash 2.0 reflects the updated scheme (which removed the green color). Dash 2.1 is a smaller size and has a few more color tweaks. The smaller size is easier to ship, and fits better in a claw machine!

Dash 2.0 and 2.1
Dash 2.0 and 2.1

Flutter Interact claw machine

有关 Dash 的信息

Dash facts

巨型 Dash 首次在 2019年 12 月 11 日纽约布鲁克林的 Flutter Interact 活动中亮相。

Mega-Dash in the office

Mega-Dash made her first appearance at the Flutter Interact event in Brooklyn, New York, on December 11, 2019.

我们的许多 YouTube 视频都有 Dart 布偶的出镜,由我们早期(且深受大家喜爱)的 Flutter 开发技术推广工程师 Emily Fortuna 配音。

Nilay and the Dash puppet

A number of our YouTube videos feature the Dash puppet, voiced by Emily Fortuna, one of our early (and much loved) Flutter Developer Advocates.

"Born to Hot Reload" jacket

Flutter Widget 目录

你可以在下方以字母顺序查看各个 Widget 的使用方法,几乎包括了所有与 Flutter 相关的 widget。除此之外你还可以查阅 核心 Widget 目录

This is an alphabetical list of nearly every widget that is bundled with Flutter. You can also browse widgets by category.

我们每周都会在 Youtube Flutter 频道 发布关于 Widget 的系列视频,你可以前去观看学习。每一个短视频都介绍了一个不同的 Flutter Widget。关于更多系列视频,也欢迎查看我们的 学习 Flutter 的视频列表

You might also want to check out our Widget of the Week video series on the Flutter YouTube channel. Each short episode features a different Flutter widget. For more video series, see our videos page.

Widget 视频的每周播放列表

Widget of the Week playlist

AbsorbPointer

A widget that absorbs pointers during hit testing. When absorbing is true, this widget prevents its subtree from receiving pointer events by terminating hit testing...

AlertDialog

Alerts are urgent interruptions requiring acknowledgement that inform the user about a situation. The AlertDialog widget implements this component.

Align

A widget that aligns its child within itself and optionally sizes itself based on the child's size.

AnimatedAlign

Animated version of Align which automatically transitions the child's position over a given duration whenever the given alignment changes.

AnimatedBuilder

A general-purpose widget for building animations. AnimatedBuilder is useful for more complex widgets that wish to include animation as part of a larger build function....

AnimatedContainer

A container that gradually changes its values over a period of time.

AnimatedCrossFade

A widget that cross-fades between two given children and animates itself between their sizes.

AnimatedDefaultTextStyle

Animated version of DefaultTextStyle which automatically transitions the default text style (the text style to apply to descendant Text widgets without explicit style) over a...

AnimatedListState

The state for a scrolling container that animates items when they are inserted or removed.

AnimatedModalBarrier

A widget that prevents the user from interacting with widgets behind itself.

AnimatedOpacity

Animated version of Opacity which automatically transitions the child's opacity over a given duration whenever the given opacity changes.

AnimatedPhysicalModel

Animated version of PhysicalModel.

AnimatedPositioned

Animated version of Positioned which automatically transitions the child's position over a given duration whenever the given position changes.

AnimatedSize

Animated widget that automatically transitions its size over a given duration whenever the given child's size changes.

AnimatedWidget

A widget that rebuilds when the given Listenable changes value.

AnimatedWidgetBaseState

A base class for widgets with implicit animations.

Appbar

A Material Design app bar. An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar.

AspectRatio

A widget that attempts to size the child to a specific aspect ratio.

AssetBundle

Asset bundles contain resources, such as images and strings, that can be used by an application. Access to these resources is asynchronous so that they...

Autocomplete

A widget for helping the user make a selection by entering some text and choosing from among a list of options.

BackdropFilter

A widget that applies a filter to the existing painted content and then paints a child. This effect is relatively expensive, especially if the filter...

Abc
Baseline

A widget that positions its child according to the child's baseline.

BottomNavigationBar

Bottom navigation bars make it easy to explore and switch between top-level views in a single tap. The BottomNavigationBar widget implements this component.

BottomSheet

Bottom sheets slide up from the bottom of the screen to reveal more content. You can call showBottomSheet() to implement a persistent bottom sheet or...

Card

A Material Design card. A card has slightly rounded corners and a shadow.

Center

A widget that centers its child within itself.

Checkbox

Checkboxes allow the user to select multiple options from a set. The Checkbox widget implements this component.

Chip

A Material Design chip. Chips represent complex entities in small blocks, such as a contact.

CircularProgressIndicator

A material design circular progress indicator, which spins to indicate that the application is busy.

ClipOval

A widget that clips its child using an oval.

ClipPath

A widget that clips its child using a path.

ClipRect

A widget that clips its child using a rectangle.

Column

Layout a list of child widgets in the vertical direction.

ConstrainedBox

A widget that imposes additional constraints on its child.

Container

A convenience widget that combines common painting, positioning, and sizing widgets.

CupertinoActionSheet

An iOS-style modal bottom action sheet to choose an option among many.

CupertinoActivityIndicator

An iOS-style activity indicator. Displays a circular 'spinner'.

CupertinoAlertDialog

An iOS-style alert dialog.

CupertinoButton

An iOS-style button.

CupertinoContextMenu

An iOS-style full-screen modal route that opens when the child is long-pressed. Used to display relevant actions for your content.

CupertinoDatePicker

An iOS-style date or date and time picker.

CupertinoDialogAction

A button typically used in a CupertinoAlertDialog.

CupertinoFullscreenDialogTransition

An iOS-style transition used for summoning fullscreen dialogs.

CupertinoNavigationBar

An iOS-style top navigation bar. Typically used with CupertinoPageScaffold.

CupertinoPageScaffold

Basic iOS style page layout structure. Positions a navigation bar and content on a background.

CupertinoPageTransition

Provides an iOS-style page transition animation.

CupertinoPicker

An iOS-style picker control. Used to select an item in a short list.

CupertinoPopupSurface

Rounded rectangle surface that looks like an iOS popup surface, such as an alert dialog or action sheet.

CupertinoScrollbar

An iOS-style scrollbar that indicates which portion of a scrollable widget is currently visible.

CupertinoSearchTextField

An iOS-style search field.

CupertinoSegmentedControl

An iOS-style segmented control. Used to select mutually exclusive options in a horizontal list.

CupertinoSlider

Used to select from a range of values.

CupertinoSlidingSegmentedControl

An iOS-13-style segmented control. Used to select mutually exclusive options in a horizontal list.

CupertinoSliverNavigationBar

An iOS-styled navigation bar with iOS-11-style large titles using slivers.

CupertinoSwitch

An iOS-style switch. Used to toggle the on/off state of a single setting.

CupertinoTabBar

An iOS-style bottom tab bar. Typically used with CupertinoTabScaffold.

CupertinoTabScaffold

Tabbed iOS app structure. Positions a tab bar on top of tabs of content.

CupertinoTabView

Root content of a tab that supports parallel navigation between tabs. Typically used with CupertinoTabScaffold.

CupertinoTextField

An iOS-style text field.

CupertinoTimerPicker

An iOS-style countdown timer picker.

CustomMultiChildLayout

A widget that uses a delegate to size and position multiple children.

CustomPaint

A widget that provides a canvas on which to draw during the paint phase.

CustomScrollView

A ScrollView that creates custom scroll effects using slivers.

CustomSingleChildLayout

A widget that defers the layout of its single child to a delegate.

DataTable

Data tables display sets of raw data. They usually appear in desktop enterprise products. The DataTable widget implements this component.

Date & Time Pickers

Date pickers use a dialog window to select a single date on mobile. Time pickers use a dialog to select a single time (in the...

DecoratedBox

A widget that paints a Decoration either before or after its child paints.

DecoratedBoxTransition

Animated version of a DecoratedBox that animates the different properties of its Decoration.

DefaultTextStyle

The text style to apply to descendant Text widgets without explicit style.

Dismissible

A widget that can be dismissed by dragging in the indicated direction. Dragging or flinging this widget in the DismissDirection causes the child to slide...

Divider

A one logical pixel thick horizontal line, with padding on either side.

DragTarget

A widget that receives data when a Draggable widget is dropped. When a draggable is dragged on top of a drag target, the drag target...

Draggable

A widget that can be dragged from to a DragTarget. When a draggable widget recognizes the start of a drag gesture, it displays a feedback...

DraggableScrollableSheet

A container for a Scrollable that responds to drag gestures by resizing the scrollable until a limit is reached, and then scrolling.

Drawer

A Material Design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application.

DropdownButton

Shows the currently selected item and an arrow that opens a menu for selecting another item.

ElevatedButton

A Material Design elevated button. A filled button whose material elevates when pressed.

ExcludeSemantics

A widget that drops all the semantics of its descendants. This can be used to hide subwidgets that would otherwise be reported but that would...

Expanded

A widget that expands a child of a Row, Column, or Flex.

ExpansionPanel

Expansion panels contain creation flows and allow lightweight editing of an element. The ExpansionPanel widget implements this component.

FadeTransition

Animates the opacity of a widget.

FittedBox

Scales and positions its child within itself according to fit.

FloatingActionButton

A floating action button is a circular icon button that hovers over content to promote a primary action in the application. Floating action buttons are...

Flow

A widget that implements the flow layout algorithm.

FlutterLogo

The Flutter logo, in widget form. This widget respects the IconTheme.

Form

An optional container for grouping together multiple form field widgets (e.g. TextField widgets).

FormField

A single form field. This widget maintains the current state of the form field, so that updates and validation errors are visually reflected in the...

FractionalTranslation

A widget that applies a translation expressed as a fraction of the box's size before painting its child.

FractionallySizedBox

A widget that sizes its child to a fraction of the total available space. For more details about the layout algorithm, see RenderFractionallySizedOverflowBox.

FutureBuilder

Widget that builds itself based on the latest snapshot of interaction with a Future.

GestureDetector

A widget that detects gestures. Attempts to recognize gestures that correspond to its non-null callbacks. If this widget has a child, it defers to that...

GridView

A grid list consists of a repeated pattern of cells arrayed in a vertical and horizontal layout. The GridView widget implements this component.

Hero

A widget that marks its child as being a candidate for hero animations.

Icon

A Material Design icon.

IconButton

An icon button is a picture printed on a Material widget that reacts to touches by filling with color (ink).

IgnorePointer

A widget that is invisible during hit testing. When ignoring is true, this widget (and its subtree) is invisible to hit testing. It still consumes...

Image

A widget that displays an image.

IndexedStack

A Stack that shows a single child from a list of children.

InteractiveViewer

A widget that enables pan and zoom interactions with its child.

IntrinsicHeight

A widget that sizes its child to the child's intrinsic height.

IntrinsicWidth

A widget that sizes its child to the child's intrinsic width.

LayoutBuilder

Builds a widget tree that can depend on the parent widget's size.

LimitedBox

A box that limits its size only when it's unconstrained.

LinearProgressIndicator

A material design linear progress indicator, also known as a progress bar.

ListBody

A widget that arranges its children sequentially along a given axis, forcing them to the dimension of the parent in the other axis.

ListTile

A single fixed-height row that typically contains some text as well as a leading or trailing icon.

ListView

A scrollable, linear list of widgets. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction....

LongPressDraggable

Makes its child draggable starting from long press.

MaterialApp

A convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design.

MediaQuery

Establishes a subtree in which media queries resolve to the given data.

MergeSemantics

A widget that merges the semantics of its descendants.

Navigator

A widget that manages a set of child widgets with a stack discipline. Many apps have a navigator near the top of their widget hierarchy...

NestedScrollView

A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.

NotificationListener

A widget that listens for Notifications bubbling up the tree.

Offstage

A widget that lays the child out as if it was in the tree, but without painting anything, without making the child available for hit...

Opacity

A widget that makes its child partially transparent.

OutlinedButton

A Material Design outlined button, essentially a TextButton with an outlined border.

OverflowBox

A widget that imposes different constraints on its child than it gets from its parent, possibly allowing the child to overflow the parent.

Padding

A widget that insets its child by the given padding.

PageView

A scrollable list that works page by page.

Placeholder

A widget that draws a box that represents where other widgets will one day be added.

PopupMenuButton

Displays a menu when pressed and calls onSelected when the menu is dismissed because an item was selected.

PositionedTransition

Animated version of Positioned which takes a specific Animation to transition the child's position from a start position to and end position over the lifetime...

Radio

Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs...

RawImage

A widget that displays a dart:ui.Image directly.

RawKeyboardListener

A widget that calls a callback whenever the user presses or releases a key on a keyboard.

RefreshIndicator

A Material Design pull-to-refresh wrapper for scrollables.

ReorderableListView

A list whose items the user can interactively reorder by dragging.

RichText

The RichText widget displays text that uses multiple different styles. The text to display is described using a tree of TextSpan objects, each of which...

RotatedBox

A widget that rotates its child by a integral number of quarter turns.

RotationTransition

Animates the rotation of a widget.

Row

Layout a list of child widgets in the horizontal direction.

Scaffold

Implements the basic Material Design visual layout structure. This class provides APIs for showing drawers, snack bars, and bottom sheets.

ScaleTransition

Animates the scale of transformed widget.

ScrollConfiguration

Controls how Scrollable widgets behave in a subtree.

Scrollable

Scrollable implements the interaction model for a scrollable widget, including gesture recognition, but does not have an opinion about how the viewport, which actually displays...

Scrollbar

A Material Design scrollbar. A scrollbar indicates which portion of a Scrollable widget is actually visible.

Semantics

A widget that annotates the widget tree with a description of the meaning of the widgets. Used by accessibility tools, search engines, and other semantic...

SimpleDialog

Simple dialogs can provide additional details or actions about a list item. For example they can display avatars icons clarifying subtext or orthogonal actions (such...

SingleChildScrollView

A box in which a single widget can be scrolled. This widget is useful when you have a single box that will normally be entirely...

SizeTransition

Animates its own size and clips and aligns the child.

SizedBox

A box with a specified size. If given a child, this widget forces its child to have a specific width and/or height (assuming values are...

SizedOverflowBox

A widget that is a specific size but passes its original constraints through to its child, which will probably overflow.

SlideTransition

Animates the position of a widget relative to its normal position.

Slider

Sliders let users select from a range of values by moving the slider thumb.

SliverAppBar

A material design app bar that integrates with a CustomScrollView.

SliverChildBuilderDelegate

A delegate that supplies children for slivers using a builder callback.

SliverChildListDelegate

A delegate that supplies children for slivers using an explicit list.

SliverFixedExtentList

A sliver that places multiple box children with the same main axis extent in a linear array.

SliverGrid

A sliver that places multiple box children in a two dimensional arrangement.

SliverList

A sliver that places multiple box children in a linear array along the main axis.

SliverPadding

A sliver that applies padding on each side of another sliver.

SliverPersistentHeader

A sliver whose size varies when the sliver is scrolled to the edge of the viewport opposite the sliver's GrowthDirection.

SliverToBoxAdapter

A sliver that contains a single box widget.

SnackBar

A lightweight message with an optional action which briefly displays at the bottom of the screen.

Stack

This class is useful if you want to overlap several children in a simple way, for example having some text and an image, overlaid with...

Stepper

A Material Design stepper widget that displays progress through a sequence of steps.

StreamBuilder

Widget that builds itself based on the latest snapshot of interaction with a Stream.

Switch

On/off switches toggle the state of a single settings option. The Switch widget implements this component.

TabBar

A Material Design widget that displays a horizontal row of tabs.

TabBarView

A page view that displays the widget which corresponds to the currently selected tab. Typically used in conjunction with a TabBar.

TabController

Coordinates tab selection between a TabBar and a TabBarView.

TabPageSelector

Displays a row of small circular indicators, one per tab. The selected tab's indicator is highlighted. Often used in conjunction with a TabBarView.

Table

A widget that uses the table layout algorithm for its children.

Abc
Text

A run of text with a single style.

TextButton

A Material Design text button. A simple flat button without a border outline.

TextField

Touching a text field places the cursor and displays the keyboard. The TextField widget implements this component.

Theme

Applies a theme to descendant widgets. A theme describes the colors and typographic choices of an application.

Tooltip

Tooltips provide text labels that help explain the function of a button or other user interface action. Wrap the button in a Tooltip widget to...

Transform

A widget that applies a transformation before painting its child.

WidgetsApp

A convenience class that wraps a number of widgets that are commonly required for an application.

Wrap

A widget that displays its children in multiple horizontal or vertical runs.

flutter: The Flutter command-line tool

目录

The flutter command-line tool is how developers (or IDEs on behalf of developers) interact with Flutter.

Here’s how you might use the flutter tool to create, analyze, test, and run an app:

$ flutter create my_app
$ cd my_app
$ flutter analyze
$ flutter test
$ flutter run lib/main.dart

To run pub commands using the flutter tool:

$ flutter pub get
$ flutter pub outdated
$ flutter pub upgrade

To view all commands that flutter supports:

$ flutter --help --verbose

To get the current version of the Flutter SDK, including its framework, engine, and tools:

$ flutter --version

flutter commands

The following table shows which commands you can use with the flutter tool:

Command Example of use More information
analyze flutter analyze -d <DEVICE_ID> Analyzes the project’s Dart source code.
Use instead of dartanalyzer.
assemble flutter assemble -o <DIRECTORY> Assemble and build flutter resources.
attach flutter attach -d <DEVICE_ID> Attach to a running application.
bash-completion flutter bash-completion Output command line shell completion setup scripts.
build flutter build <DIRECTORY> Flutter build commands.
channel flutter channel <CHANNEL_NAME> List or switch flutter channels.
config flutter config --build-dir=<DIRECTORY> Configure Flutter settings. To remove a setting, configure it to an empty string.
create flutter create <DIRECTORY> Creates a new project.
devices flutter devices -d <DEVICE_ID> List all connected devices.
doctor flutter doctor Show information about the installed tooling.
downgrade flutter downgrade Downgrade Flutter to the last active version for the current channel.
drive flutter drive Runs Flutter Driver tests for the current project.
emulators flutter emulators List, launch and create emulators.
format flutter format <DIRECTORY|DART_FILE> Formats Flutter source code.
Use instead of dartfmt.
gen-l10n flutter gen-l10n <DIRECTORY> Generate localizations for the Flutter project.
install flutter install -d <DEVICE_ID> Install a Flutter app on an attached device.
logs flutter logs Show log output for running Flutter apps.
precache flutter precache <ARGUMENTS> Populates the Flutter tool’s cache of binary artifacts.
pub flutter pub <PUB_COMMAND> Works with packages.
Use instead of pub.
run flutter run <DART_FILE> Runs a Flutter program.
symbolize flutter symbolize --input=<STACK_TRACK_FILE> Symbolize a stack trace from the AOT compiled flutter application.
test flutter test [<DIRECTORY|DART_FILE>] Runs tests in this package.
Use instead of pub run test.
upgrade flutter upgrade Upgrade your copy of Flutter.

For additional help on any of the commands, enter flutter help <command> or follow the links in the More information column. You can also get details on pub commands — for example, flutter help pub outdated.